# Simple PostGIS Reverse Geocoder
<!-- markdownlint-disable -->
<p align="center">
<img src="https://raw.githubusercontent.com/hotosm/pg-nearest-city/refs/heads/main/docs/images/hot_logo.png" style="width: 200px;" alt="HOT"></a>
</p>
<p align="center">
<em>Given a geopoint, find the nearest city using PostGIS (reverse geocode).</em>
</p>
<p align="center">
<a href="https://github.com/hotosm/pg-nearest-city/actions/workflows/docs.yml" target="_blank">
<img src="https://github.com/hotosm/pg-nearest-city/actions/workflows/docs.yml/badge.svg" alt="Publish Docs">
</a>
<a href="https://github.com/hotosm/pg-nearest-city/actions/workflows/publish.yml" target="_blank">
<img src="https://github.com/hotosm/pg-nearest-city/actions/workflows/publish.yml/badge.svg" alt="Publish">
</a>
<a href="https://github.com/hotosm/pg-nearest-city/actions/workflows/pytest.yml" target="_blank">
<img src="https://github.com/hotosm/pg-nearest-city/actions/workflows/pytest.yml/badge.svg?branch=main" alt="Test">
</a>
<a href="https://pypi.org/project/pg-nearest-city" target="_blank">
<img src="https://img.shields.io/pypi/v/pg-nearest-city?color=%2334D058&label=pypi%20package" alt="Package version">
</a>
<a href="https://pypistats.org/packages/pg-nearest-city" target="_blank">
<img src="https://img.shields.io/pypi/dm/pg-nearest-city.svg" alt="Downloads">
</a>
<a href="https://github.com/hotosm/pg-nearest-city/blob/main/LICENSE.md" target="_blank">
<img src="https://img.shields.io/github/license/hotosm/pg-nearest-city.svg" alt="License">
</a>
</p>
---
📖 **Documentation**: <a href="https://hotosm.github.io/pg-nearest-city/" target="_blank">https://hotosm.github.io/pg-nearest-city/</a>
🖥️ **Source Code**: <a href="https://github.com/hotosm/pg-nearest-city" target="_blank">https://github.com/hotosm/pg-nearest-city</a>
---
<!-- markdownlint-enable -->
## Why do we need this?
This package was developed primarily as a **basic** reverse geocoder for use within
web frameworks (APIs) that **have an existing PostGIS connection to utilise**.
Simple alternatives:
- The reverse geocoding package in Python [here](https://github.com/thampiman/reverse-geocoder)
is probably the original and canonincal implementation using K-D tree.
- However, it's a bit outdated now, with numerous unattended pull
requests and uses an unfavourable multiprocessing-based approach.
- It leaves a large memory footprint of approximately 260MB to load the
K-D tree in memory (see [benchmarks](./benchmark-results.md)), which
remains there: an unacceptable compromise for a web server for such a
small amount of functionality.
- The package [here](https://github.com/richardpenman/reverse_geocode) is an excellent
revamp of the package above, and possibly the best choice in many scenarios,
particularly if PostGIS is not available.
The pg-nearest-city approach:
- Is approximately ~20x more performant (45ms --> 2ms).
- Has a small ~8MB memory footprint, compared to ~260MB.
- However it has a one-time initialisation penalty of approximately 16s
to load the data into the database (which could be handled at
web server startup).
See [benchmarks](./benchmark-results.md) for more details.
> [!NOTE]
> We don't discuss web based geocoding services here, such as Nominatim, as simple
> offline reverse-geocoding has two purposes:
>
> - Reduced latency, when very precise locations are not required.
> - Reduced load on free services such as Nominatim (particularly when running
> in automated tests frequently).
### Priorities
- Lightweight package size.
- Minimal memory footprint.
- High performance.
### How This Package Works
- Ingest geonames.org data for cities over 1000 population.
- Create voronoi polygons based on city geopoints.
- Bundle the voronoi data with this package and load into Postgis.
- Query the loaded voronoi data with a given geopoint, returning the city.
The diagram below should give a good indication for how this works:

## Usage
### Install
Distributed as a pip package on PyPi:
```bash
pip install pg-nearest-city
# or use your dependency manager of choice
```
### Run The Code
#### Async
```python
from pg_nearest_city import AsyncNearestCity
# Existing code to get db connection, say from API endpoint
db = await get_db_connection()
async with AsyncNearestCity(db) as geocoder:
location = await geocoder.query(40.7128, -74.0060)
print(location.city)
# "New York City"
print(location.country)
# "USA"
```
#### Sync
```python
from pg_nearest_city import NearestCity
# Existing code to get db connection, say from API endpoint
db = get_db_connection()
with NearestCity(db) as geocoder:
location = geocoder.query(40.7128, -74.0060)
print(location.city)
# "New York City"
print(location.country)
# "USA"
```
#### Create A New DB Connection
- If your app upstream already has a psycopg connection, this can be
passed through.
- If you require a new database connection, the connection parameters
can be defined as DbConfig object variables:
```python
from pg_nearest_city import DbConfig, AsyncNearestCity
db_config = DbConfig(
dbname="db1",
user="user1",
password="pass1",
host="localhost",
port="5432",
)
async with AsyncNearestCity(db_config) as geocoder:
location = await geocoder.query(40.7128, -74.0060)
```
- Or alternatively as variables from your system environment:
```dotenv
PGNEAREST_DB_NAME=cities
PGNEAREST_DB_USER=cities
PGNEAREST_DB_PASSWORD=somepassword
PGNEAREST_DB_HOST=localhost
PGNEAREST_DB_PORT=5432
```
then
```python
from pg_nearest_city import AsyncNearestCity
async with AsyncNearestCity() as geocoder:
location = await geocoder.query(40.7128, -74.0060)
```
## Testing
Run the tests with:
```bash
docker compose run --rm code pytest
```
## Benchmarks
Run the benchmarks with:
```bash
docker compose run --rm benchmark
```
Raw data
{
"_id": null,
"home_page": null,
"name": "pg-nearest-city",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.10",
"maintainer_email": null,
"keywords": "geocode, nearest, postgis, reverse, reverse-geocode",
"author": "Emir Fabio Cognigni",
"author_email": "Sam Woodcock <sam.woodcock@hotosm.org>",
"download_url": "https://files.pythonhosted.org/packages/62/8b/7a40b079cbd478d5e76a11eb1ecab6d1b2a491992faf6d455a6e5b18fff2/pg_nearest_city-0.2.1.tar.gz",
"platform": null,
"description": "# Simple PostGIS Reverse Geocoder\n\n<!-- markdownlint-disable -->\n<p align=\"center\">\n <img src=\"https://raw.githubusercontent.com/hotosm/pg-nearest-city/refs/heads/main/docs/images/hot_logo.png\" style=\"width: 200px;\" alt=\"HOT\"></a>\n</p>\n<p align=\"center\">\n <em>Given a geopoint, find the nearest city using PostGIS (reverse geocode).</em>\n</p>\n<p align=\"center\">\n <a href=\"https://github.com/hotosm/pg-nearest-city/actions/workflows/docs.yml\" target=\"_blank\">\n <img src=\"https://github.com/hotosm/pg-nearest-city/actions/workflows/docs.yml/badge.svg\" alt=\"Publish Docs\">\n </a>\n <a href=\"https://github.com/hotosm/pg-nearest-city/actions/workflows/publish.yml\" target=\"_blank\">\n <img src=\"https://github.com/hotosm/pg-nearest-city/actions/workflows/publish.yml/badge.svg\" alt=\"Publish\">\n </a>\n <a href=\"https://github.com/hotosm/pg-nearest-city/actions/workflows/pytest.yml\" target=\"_blank\">\n <img src=\"https://github.com/hotosm/pg-nearest-city/actions/workflows/pytest.yml/badge.svg?branch=main\" alt=\"Test\">\n </a>\n <a href=\"https://pypi.org/project/pg-nearest-city\" target=\"_blank\">\n <img src=\"https://img.shields.io/pypi/v/pg-nearest-city?color=%2334D058&label=pypi%20package\" alt=\"Package version\">\n </a>\n <a href=\"https://pypistats.org/packages/pg-nearest-city\" target=\"_blank\">\n <img src=\"https://img.shields.io/pypi/dm/pg-nearest-city.svg\" alt=\"Downloads\">\n </a>\n <a href=\"https://github.com/hotosm/pg-nearest-city/blob/main/LICENSE.md\" target=\"_blank\">\n <img src=\"https://img.shields.io/github/license/hotosm/pg-nearest-city.svg\" alt=\"License\">\n </a>\n</p>\n\n---\n\n\ud83d\udcd6 **Documentation**: <a href=\"https://hotosm.github.io/pg-nearest-city/\" target=\"_blank\">https://hotosm.github.io/pg-nearest-city/</a>\n\n\ud83d\udda5\ufe0f **Source Code**: <a href=\"https://github.com/hotosm/pg-nearest-city\" target=\"_blank\">https://github.com/hotosm/pg-nearest-city</a>\n\n---\n\n<!-- markdownlint-enable -->\n\n## Why do we need this?\n\nThis package was developed primarily as a **basic** reverse geocoder for use within\nweb frameworks (APIs) that **have an existing PostGIS connection to utilise**.\n\nSimple alternatives:\n\n- The reverse geocoding package in Python [here](https://github.com/thampiman/reverse-geocoder)\n is probably the original and canonincal implementation using K-D tree.\n - However, it's a bit outdated now, with numerous unattended pull\n requests and uses an unfavourable multiprocessing-based approach.\n - It leaves a large memory footprint of approximately 260MB to load the\n K-D tree in memory (see [benchmarks](./benchmark-results.md)), which\n remains there: an unacceptable compromise for a web server for such a\n small amount of functionality.\n- The package [here](https://github.com/richardpenman/reverse_geocode) is an excellent\n revamp of the package above, and possibly the best choice in many scenarios,\n particularly if PostGIS is not available.\n\nThe pg-nearest-city approach:\n\n- Is approximately ~20x more performant (45ms --> 2ms).\n- Has a small ~8MB memory footprint, compared to ~260MB.\n- However it has a one-time initialisation penalty of approximately 16s\n to load the data into the database (which could be handled at\n web server startup).\n\nSee [benchmarks](./benchmark-results.md) for more details.\n\n> [!NOTE]\n> We don't discuss web based geocoding services here, such as Nominatim, as simple\n> offline reverse-geocoding has two purposes:\n>\n> - Reduced latency, when very precise locations are not required.\n> - Reduced load on free services such as Nominatim (particularly when running\n> in automated tests frequently).\n\n### Priorities\n\n- Lightweight package size.\n- Minimal memory footprint.\n- High performance.\n\n### How This Package Works\n\n- Ingest geonames.org data for cities over 1000 population.\n- Create voronoi polygons based on city geopoints.\n- Bundle the voronoi data with this package and load into Postgis.\n- Query the loaded voronoi data with a given geopoint, returning the city.\n\nThe diagram below should give a good indication for how this works:\n\n\n\n## Usage\n\n### Install\n\nDistributed as a pip package on PyPi:\n\n```bash\npip install pg-nearest-city\n# or use your dependency manager of choice\n```\n\n### Run The Code\n\n#### Async\n\n```python\nfrom pg_nearest_city import AsyncNearestCity\n\n# Existing code to get db connection, say from API endpoint\ndb = await get_db_connection()\n\nasync with AsyncNearestCity(db) as geocoder:\n location = await geocoder.query(40.7128, -74.0060)\n\nprint(location.city)\n# \"New York City\"\nprint(location.country)\n# \"USA\"\n```\n\n#### Sync\n\n```python\nfrom pg_nearest_city import NearestCity\n\n# Existing code to get db connection, say from API endpoint\ndb = get_db_connection()\n\nwith NearestCity(db) as geocoder:\n location = geocoder.query(40.7128, -74.0060)\n\nprint(location.city)\n# \"New York City\"\nprint(location.country)\n# \"USA\"\n```\n\n#### Create A New DB Connection\n\n- If your app upstream already has a psycopg connection, this can be\n passed through.\n- If you require a new database connection, the connection parameters\n can be defined as DbConfig object variables:\n\n```python\nfrom pg_nearest_city import DbConfig, AsyncNearestCity\n\ndb_config = DbConfig(\n dbname=\"db1\",\n user=\"user1\",\n password=\"pass1\",\n host=\"localhost\",\n port=\"5432\",\n)\n\nasync with AsyncNearestCity(db_config) as geocoder:\n location = await geocoder.query(40.7128, -74.0060)\n```\n\n- Or alternatively as variables from your system environment:\n\n```dotenv\nPGNEAREST_DB_NAME=cities\nPGNEAREST_DB_USER=cities\nPGNEAREST_DB_PASSWORD=somepassword\nPGNEAREST_DB_HOST=localhost\nPGNEAREST_DB_PORT=5432\n```\n\nthen\n\n```python\nfrom pg_nearest_city import AsyncNearestCity\n\nasync with AsyncNearestCity() as geocoder:\n location = await geocoder.query(40.7128, -74.0060)\n```\n\n## Testing\n\nRun the tests with:\n\n```bash\ndocker compose run --rm code pytest\n```\n\n## Benchmarks\n\nRun the benchmarks with:\n\n```bash\ndocker compose run --rm benchmark\n```\n",
"bugtrack_url": null,
"license": "GPL-3.0-only",
"summary": "Given a geopoint, find the nearest city using PostGIS (reverse geocode).",
"version": "0.2.1",
"project_urls": {
"documentation": "https://hotosm.github.io/pg-nearest-city",
"homepage": "https://github.com/hotosm/pg-nearest-city",
"repository": "https://github.com/hotosm/pg-nearest-city"
},
"split_keywords": [
"geocode",
" nearest",
" postgis",
" reverse",
" reverse-geocode"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "c28ee66698a7bf16304aaa4caa20b590b85d5c7123d8b824b46941979cdd0e17",
"md5": "10f32069fec9b66a07bff0965e852275",
"sha256": "631f0115105ad6e1e65758ab5cedcf56ed2a5b048ca13ecc152caabb748cb177"
},
"downloads": -1,
"filename": "pg_nearest_city-0.2.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "10f32069fec9b66a07bff0965e852275",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10",
"size": 13147516,
"upload_time": "2025-02-17T23:32:29",
"upload_time_iso_8601": "2025-02-17T23:32:29.709111Z",
"url": "https://files.pythonhosted.org/packages/c2/8e/e66698a7bf16304aaa4caa20b590b85d5c7123d8b824b46941979cdd0e17/pg_nearest_city-0.2.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "628b7a40b079cbd478d5e76a11eb1ecab6d1b2a491992faf6d455a6e5b18fff2",
"md5": "420af91102588f3684f3e5626c6d0906",
"sha256": "a026d166624db748d8bc44e4e5c0433df125c523d5b626d2a96e8e1cafe8313a"
},
"downloads": -1,
"filename": "pg_nearest_city-0.2.1.tar.gz",
"has_sig": false,
"md5_digest": "420af91102588f3684f3e5626c6d0906",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10",
"size": 13324955,
"upload_time": "2025-02-17T23:32:33",
"upload_time_iso_8601": "2025-02-17T23:32:33.733847Z",
"url": "https://files.pythonhosted.org/packages/62/8b/7a40b079cbd478d5e76a11eb1ecab6d1b2a491992faf6d455a6e5b18fff2/pg_nearest_city-0.2.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-02-17 23:32:33",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "hotosm",
"github_project": "pg-nearest-city",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "pg-nearest-city"
}