├── .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 | [![Downloads](https://img.shields.io/npm/dm/geo2city.svg)](https://www.npmjs.com/package/geo2city) [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC) [![License: CC BY 4.0](https://img.shields.io/badge/License-CC%20BY%204.0-lightgrey.svg)](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 | --------------------------------------------------------------------------------