├── .gitignore
├── .npmignore
├── .npmrc
├── LICENSE.txt
├── README.md
├── cjs
├── extract.js
└── index.js
├── package.json
├── sqlite
├── flags.js
├── import.sh
└── import.sql
├── test
└── index.js
└── worldcities.csv
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .nyc_output
3 | node_modules/
4 | worldcities.db
5 | worldcities.db.zip
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .nyc_output
3 | .travis.yml
4 | node_modules/
5 | sqlite/
6 | test/
7 | worldcities.csv
8 | worldcities.db
9 | !worldcities.db.zip
10 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | ISC License
2 |
3 | Copyright (c) 2020, Andrea Giammarchi, @WebReflection
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any
6 | purpose with or without fee is hereby granted, provided that the above
7 | copyright notice and this permission notice appear in all copies.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15 | PERFORMANCE OF THIS SOFTWARE.
16 |
17 |
18 |
19 | CC BY 4.0 License
20 |
21 | This license is a legal document designed to protect your rights and the rights of the Pareto Software, LLC, the owner of Simplemaps.com. Please read it carefully. Purchasing or downloading a data product constitutes acceptance of this license.
22 |
23 | Description of Product and Parties: This license is a contract between you (hereafter, the Customer) and Pareto Software, LLC (hereafter, the Provider) regarding the use and/or sale of an collection of geographic data (hereafter, the Database).
24 |
25 | Ownership of Database: All rights to the Database are owned by the Provider. The Database is a cleaned and curated collection of geographic facts and the Provider retains all rights to the Database afforded by the law. Ownership of any intellectual property generated by the Provider while performing custom modifications to the Database for a Customer (with or without payment) is retained by the Provider.
26 |
27 | License: Customers who purchase a license are allowed to use the database for projects that benefit their organization or that their organization oversees. Attribution is not required. The Customer is allowed to query the database to power privately or publicly facing applications. The Customer is allowed to make copies and backups of the data. The Customer may not publicly redistribute the Database without prior written permission. Customers can transfer their license to a third-party, at the sole discretion of the Provider, by submitting a request via email.
28 |
29 | Free US Zip Code Database: The Provider offers a free version of the US Zip Code Database. This Database is offered free of charge conditional on a link back to https://simplemaps.com/data/us-zips. This backlink must come from a public webpage where the Customer is using the data. If the Customer uses the data internally, the backlink must be placed on the organization's website on a page that can be easily found though links on the root domain. The link must be clearly visible to the human eye. The backlink must be placed before the Customer uses the Database in production.
30 |
31 | Free US Cities Database: The Provider offers a free version of the US Cities Database. This Database is offered free of charge conditional on a link back to https://simplemaps.com/data/us-cities. This backlink must come from a public webpage where the Customer is using the data. If the Customer uses the data internally, the backlink must be placed on the organization's website on a page that can be easily found though links on the root domain. The link must be clearly visible to the human eye. The backlink must be placed before the Customer uses the Database in production.
32 |
33 | Basic World Cities Database: The Provider offers a Basic World Cities Database free of charge. This database is licensed under the Creative Commons Attribution 4.0 license as described at: https://creativecommons.org/licenses/by/4.0/.
34 |
35 | Comprehensive and Pro World Cities Database Density Data: The Comprehensive and Pro World Cities Databases includes density estimates from The Center for International Earth Science Information Network - CIESIN - Columbia University. 2016. Gridded Population of the World, Version 4 (GPWv4): Population Count. Palisades, NY: NASA Socioeconomic Data and Applications Center (SEDAC). http://dx.doi.org/10.7927/H4X63JVC. Accessed June 2017. The density estimates are include under the Creative Commons Attribution 4.0 International License. The Provider places no additional restrictions on the use or distribution of the density data.
36 |
37 | Guarantee: The Provider guarantees that for the period of thirty (30) days from the purchase of a License that the Customer shall, upon request, be refunded their actual purchase price within a reasonable period of time. The Customer acknowledges that receipt of a refund constitutes a termination of their License to use the Database. In the event of a Refund, the Customer promises to delete the Database immediately. Refunds after the period of thirty (30) days shall be at the sole discretion of the Provider.
38 |
39 | LIMITATION OF LIABILITY: THE DATABASE IS SOLD "AS IS" AND "WITH ALL FAULTS". THE PROVIDER MAKES NO WARRANTY THAT IT IS FREE OF DEFECTS OR IS SUITABLE FOR ANY PARTICULAR PURPOSE. IN NO EVENT SHALL THE PROVIDER BE RESPONSIBLE FOR LOSS OR DAMAGES ARRISING FROM THE INSTALLATION OR USE OF THE DATABASE, INCLUDING BUT NOT LIMITED TO ANY INDIRECT, PUNITIVE, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES. THE CUSTOMER IS SOLELY RESPONSIBLE FOR ENSURING THAT THEIR USE OF THE DATABASE IS IN ACCORDANCE WITH THE LAW OF THEIR JURISDICTION.
40 |
41 | PROHIBITION OF ILLEGAL USE: USE OF THE DATABASE WHICH IS CONTRARY TO THE LAW IS PROHIBITED, AND IMMEDIATELY TERMINATES THE CUSTOMER'S LICENSE TO USE THE DATABASE.
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # geo2city
2 |
3 | [](https://www.npmjs.com/package/geo2city) [](https://opensource.org/licenses/ISC) [](https://creativecommons.org/licenses/by/4.0/)
4 |
5 | A tiny, portable, offline search, ip, and reverse geocode, also used in [Life Diary ❤️](https://github.com/WebReflection/life-diary#readme), based on [simplemaps.com](https://simplemaps.com/data/world-cities)'s *World Cities Database* basic data.
6 |
7 | ```js
8 | import {ip, search, reverse} from 'geo2city';
9 |
10 | search('Berlin, Germany').then(console.log);
11 | // result (undefined if not found)
12 | [ 52.5167, 13.3833 ]
13 |
14 | reverse([52.52437, 13.41053]).then(console.log);
15 | // result (undefined if not found)
16 | {
17 | latitude: 52.5167,
18 | longitude: 13.3833,
19 | iso2: 'DE',
20 | iso3: 'DEU',
21 | flag: '🇩🇪',
22 | country: 'Germany',
23 | city: 'Berlin'
24 | }
25 |
26 | // ⚠ requires geoiplookup (via geoip)
27 | // and geoip-database-extra
28 | ip('216.58.197.78').then(console.log);
29 | // result (undefined if not found)
30 | {
31 | latitude: 37.4,
32 | longitude: -122.0796,
33 | iso2: 'US',
34 | iso3: 'USA',
35 | flag: '🇺🇸',
36 | country: 'United States',
37 | city: 'Mountain View'
38 | }
39 | ```
40 |
41 |
42 |
43 | ## Details
44 |
45 | Geo search and reverse geocode is complicated and expensive, and it usually requires some API or network access to be performed, with all usual limitations.
46 |
47 | This module takes a different approach, [it ships a pre-optimized *SQLite* database](https://webreflection.medium.com/shipping-npm-modules-with-sqlite-4f0e9eccc3c1) which, once zipped, is no more than 700K (5MB once unzipped), and it can be used offline.
48 |
49 |
50 | ### Features
51 |
52 | * *26563* cities and related countries
53 | * country name, *iso2*, *iso3*, and *emoji* flag, per each country
54 | * reverse search via `geo2city.reverse([latitude, longitude])` with *nearest city* approximation
55 | * IPv4 reverse search via `geo2city.ip('1.1.1.1')` with *nearest city* approximation
56 | * full text search via `geo2city.search('City, Country Name or ISO')` with highest ranked result
57 |
58 |
59 |
60 | ## Attribution
61 |
62 | The *World Cities Database* has a [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license and requires a backlink to [simplemaps.com](https://simplemaps.com/data/world-cities), example:
63 |
64 | ```html
65 | Geo data by simplemaps
66 | ```
67 |
68 | The [social media image](https://simplemaps.com/static/img/data/world-cities/viz/basic.png) is also readapted from simplemaps.
69 |
70 |
71 |
72 | ## About Pro / Comprehensive Database
73 |
74 | Unfortunately, these versions of the database don't allow redistribution, but if you fork this project and run `npm i` after, then you change `worldcities.csv` with the *Pro* or *Comprehensive* database *CSV* version, and then you run `npm run import` before running `npm run postinstall`, you should have a working copy of *geo2city* pointing at a much more accurate dataset.
75 |
76 | The *country* table would likely be the same, but the *city* one should contain all millions cities offered by *simplemaps*.
77 |
78 | To succeed, you need any *Linux* or *macOS* with *sqlite3* and *zip* installed, however, I am not planning to support these versions, or provide help with these, because these are out of scope for this project.
79 |
80 | **P.S.** as I haven't tried myself, it is possible that the *worldcities_csv* table in `sqlite/import.sql` should be modified to contain all fields provided by the bigger `.csv` file, but as long as field names are the same for the interested data, everything should go rather smoothly.
81 |
--------------------------------------------------------------------------------
/cjs/extract.js:
--------------------------------------------------------------------------------
1 | const {unlink} = require('fs');
2 | const {dirname, join} = require('path');
3 |
4 | const extract = require('extract-zip');
5 | const {Database} = require('sqlite3');
6 | const SQLiteTag = require('sqlite-tag');
7 |
8 | const worldcities = 'worldcities.db';
9 | const dir = dirname(__dirname);
10 | const zip = join(dir, worldcities + '.zip');
11 |
12 | extract(zip, {dir}).then(() => {
13 | unlink(zip, error => {
14 | if (error) {
15 | console.error('Unable to unzip ' + zip);
16 | process.exit(1);
17 | }
18 | const db = new Database(join(dir, worldcities));
19 | const {query} = SQLiteTag(db);
20 | query`CREATE VIRTUAL TABLE search USING FTS5(place, latitude, longitude)`.then(() => {
21 | query`
22 | INSERT INTO search(place, latitude, longitude) SELECT
23 | (
24 | worldcities_city.city || ' ' ||
25 | worldcities_country.iso2 || ' ' ||
26 | worldcities_country.iso3 || ' ' ||
27 | worldcities_country.country
28 | ),
29 | worldcities.latitude,
30 | worldcities.longitude
31 | FROM
32 | worldcities,
33 | worldcities_country,
34 | worldcities_city
35 | WHERE
36 | worldcities_country.id = worldcities.country
37 | AND
38 | worldcities_city.id = worldcities.city
39 | `.then(() => db.close());
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/cjs/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {join} = require('path');
4 | const {exec} = require('child_process');
5 |
6 | const {Database} = require('sqlite3');
7 | const SQLiteTag = require('sqlite-tag');
8 |
9 | const db = new Database(join(__dirname, '..', 'worldcities.db'));
10 | const {get} = SQLiteTag(db);
11 |
12 | /** @typedef {number} latitude */
13 | /** @typedef {number} longitude */
14 | /** @typedef {Array} Coordinates */
15 | /** @typedef {{
16 | * latitude: latitude,
17 | * longitude: longitude,
18 | * iso2: string,
19 | * iso3: string,
20 | * flag: string,
21 | * country: string,
22 | * city: string
23 | * }} GeoData
24 | */
25 |
26 |
27 | /**
28 | * Given an array of latitude and longitude numbers, returns city related data,
29 | * if any, or undefined.
30 | * @param {Coordinates} coordinates coordinates to reverse geocode
31 | * @return {Promise}
32 | */
33 | exports.reverse = ([latitude, longitude]) => get`
34 | SELECT
35 | worldcities.latitude,
36 | worldcities.longitude,
37 | worldcities_country.iso2,
38 | worldcities_country.iso3,
39 | worldcities_country.flag,
40 | worldcities_country.country,
41 | worldcities_city.city
42 | FROM
43 | worldcities,
44 | worldcities_country,
45 | worldcities_city
46 | WHERE
47 | worldcities.country = worldcities_country.id
48 | AND
49 | worldcities.city = worldcities_city.id
50 | ORDER BY (
51 | (${latitude} - worldcities.latitude) * (${latitude} - worldcities.latitude) +
52 | (${longitude} - worldcities.longitude) * (${longitude} - worldcities.longitude)
53 | )
54 | LIMIT 1
55 | `;
56 |
57 | /**
58 | * Given a generic search string, optionally comma separated, returns the
59 | * nearest city coordinates, if any, or undefined.
60 | * @param {string} search string to retrieve the nearest city coordinates
61 | * @return {Promise}
62 | */
63 | exports.search = search => get`
64 | SELECT latitude, longitude FROM search
65 | WHERE place MATCH ${
66 | (search.trim() || '?').toLowerCase().split(/\s*,\s*/).join(' OR ')
67 | }
68 | ORDER BY rank
69 | LIMIT 1
70 | `.then(geo => geo && [geo.latitude, geo.longitude]);
71 |
72 | /**
73 | * Given a generic IPv4 address, returns city related data, if any,
74 | * or undefined.
75 | * @param {string} IPv4 address to search via geoiplookup
76 | * @return {Promise}
77 | */
78 | exports.ip = IPv4 => new Promise(resolve => {
79 | if (/^(?:\d+\.){3}\d+$/.test(IPv4))
80 | exec(`geoiplookup ${IPv4}`, (error, stdout) => {
81 | if (!error) {
82 | const [_, latitude, longitude] = stdout.match(
83 | /(?:[^,]+?,\s*){5}(-?\d+(?:\.\d+)),\s*(-?\d+(?:\.\d+)),/
84 | ) || ['', '', ''];
85 | if (latitude || longitude) {
86 | exports.reverse([
87 | parseFloat(latitude),
88 | parseFloat(longitude)
89 | ]).then(resolve);
90 | return;
91 | }
92 | }
93 | resolve();
94 | });
95 | else
96 | resolve();
97 | });
98 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "geo2city",
3 | "version": "0.5.0",
4 | "description": "A tiny, portable, offline search and reverse geocode",
5 | "main": "./cjs/index.js",
6 | "scripts": {
7 | "import": "bash ./sqlite/import.sh ~/Downloads/IP2LOCATION-LITE-DB5.CSV.ZIP",
8 | "test": "node test/index.js",
9 | "postinstall": "node ./cjs/extract.js"
10 | },
11 | "keywords": [
12 | "sqlite",
13 | "offline",
14 | "reverse",
15 | "geocode"
16 | ],
17 | "author": "Andrea Giammarchi",
18 | "license": "SEE LICENSE IN LICENSE.txt",
19 | "dependencies": {
20 | "extract-zip": "^2.0.1",
21 | "sqlite-tag": "^1.1.1",
22 | "sqlite3": "^4.2.0"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "git+https://github.com/WebReflection/geo2city.git"
27 | },
28 | "bugs": {
29 | "url": "https://github.com/WebReflection/geo2city/issues"
30 | },
31 | "homepage": "https://github.com/WebReflection/geo2city#readme",
32 | "devDependencies": {
33 | "@ideditor/country-coder": "^5.0.3"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/sqlite/flags.js:
--------------------------------------------------------------------------------
1 | const {join} = require('path');
2 |
3 | const {emojiFlag} = require('@ideditor/country-coder');
4 | const {Database} = require('sqlite3');
5 | const SQLiteTag = require('sqlite-tag');
6 |
7 | const db = new Database(join(__dirname, '..', 'worldcities.db'));
8 |
9 | const {all, query} = SQLiteTag(db);
10 |
11 | all`SELECT id, iso3 FROM worldcities_country`.then(results => {
12 | const all = [];
13 | for (const {id, iso3} of results)
14 | all.push(query`UPDATE worldcities_country SET flag = ${emojiFlag(iso3)} WHERE id = ${id}`);
15 | Promise.all(all).then(() => db.close());
16 | });
17 |
--------------------------------------------------------------------------------
/sqlite/import.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | sqlite3 ./sqlite/worldcities.db -init ./sqlite/import.sql '.exit'
4 |
5 | mv ./sqlite/worldcities.db ./
6 |
7 | node ./sqlite/flags.js
8 |
9 | zip ./worldcities.db.zip -9 ./worldcities.db
10 |
11 | npm run test
12 |
13 | rm ./worldcities.db
14 |
--------------------------------------------------------------------------------
/sqlite/import.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE worldcities_csv (
2 | city VARCHAR(128),
3 | city_ascii VARCHAR(128),
4 | lat FLOAT,
5 | lng FLOAT,
6 | country VARCHAR(64),
7 | iso2 CHAR(2),
8 | iso3 CHAR(3),
9 | admin_name VARCHAR(128),
10 | capital VARCHAR(128),
11 | population INTEGER,
12 | id INTEGER
13 | );
14 |
15 | -- https://simplemaps.com/data/world-cities
16 | .mode csv
17 | .import ./worldcities.csv worldcities_csv
18 |
19 | CREATE TABLE worldcities_country (
20 | id INTEGER NOT NULL PRIMARY KEY,
21 | iso2 CHAR(2),
22 | iso3 CHAR(3),
23 | flag VARCHAR(4),
24 | country VARCHAR(64)
25 | );
26 |
27 | INSERT INTO worldcities_country (iso2, iso3, country) SELECT DISTINCT(worldcities_csv.iso2), worldcities_csv.iso3, worldcities_csv.country FROM worldcities_csv;
28 |
29 | CREATE TABLE worldcities_city (
30 | id INTEGER NOT NULL PRIMARY KEY,
31 | city VARCHAR(128)
32 | );
33 |
34 | INSERT INTO worldcities_city (city) SELECT DISTINCT(worldcities_csv.city) FROM worldcities_csv;
35 |
36 | CREATE TABLE worldcities (
37 | latitude FLOAT,
38 | longitude FLOAT,
39 | country INTEGER,
40 | city INTEGER
41 | );
42 |
43 | INSERT INTO worldcities (latitude, longitude, country, city) SELECT
44 | worldcities_csv.lat,
45 | worldcities_csv.lng,
46 | worldcities_country.id,
47 | worldcities_city.id
48 | FROM
49 | worldcities_country,
50 | worldcities_city,
51 | worldcities_csv
52 | WHERE
53 | worldcities_country.country = worldcities_csv.country
54 | AND
55 | worldcities_city.city = worldcities_csv.city
56 | ;
57 |
58 | SELECT COUNT(*) FROM worldcities_csv;
59 | SELECT COUNT(*) FROM worldcities;
60 | SELECT COUNT(*) FROM worldcities_city;
61 | SELECT COUNT(*) FROM worldcities_country;
62 |
63 | DROP TABLE worldcities_csv;
64 |
65 | .exit
66 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | const {ip, reverse, search} = require('../cjs');
2 |
3 | console.time('reverse');
4 | reverse([52.5167, 13.3833]).then(result => {
5 | console.timeEnd('reverse');
6 | console.log(result);
7 | });
8 |
9 | console.time('search');
10 | search('Berlin, Germany').then(result => {
11 | console.timeEnd('search');
12 | console.log(result);
13 | }, Object);
14 |
15 | console.time('ip');
16 | ip('216.58.197.78').then(result => {
17 | console.timeEnd('ip');
18 | console.log(result);
19 | });
20 |
21 |
--------------------------------------------------------------------------------