Skip to content

fix: replace Nominatim reverse geocoding with pg-nearest-city#7287

Open
munzzyy wants to merge 1 commit into
hotosm:developfrom
munzzyy:feature/6905-replace-nominatim-with-pg-nearest-city
Open

fix: replace Nominatim reverse geocoding with pg-nearest-city#7287
munzzyy wants to merge 1 commit into
hotosm:developfrom
munzzyy:feature/6905-replace-nominatim-with-pg-nearest-city

Conversation

@munzzyy

@munzzyy munzzyy commented Jul 5, 2026

Copy link
Copy Markdown

What type of PR is this? (check all applicable)

  • 🍕 Feature
  • 🐛 Bug Fix
  • 📝 Documentation
  • 🧑‍💻 Refactor
  • ✅ Test
  • 🤖 Build or CI
  • ❓ Other (please specify)

Related Issue

Fixes #6905

Describe this PR

Project.set_country_info reverse-geocodes a project's centroid by making a blocking requests.get call to the public Nominatim server, inside an async FastAPI request path. That's an external dependency for something that doesn't need one, it blocks the event loop while it waits on the network, and it adds load to a server the OSM community donates for free.

This swaps that call for pg-nearest-city, the local PostGIS-backed reverse geocoder HOTOSM already built for Field-TM. It resolves the nearest city (and its country) straight from the database with a spatial query, no outbound HTTP, no external service.

Changes:

  • Added pg-nearest-city>=2.0.0 to pyproject.toml (pulls in psycopg as a dependency; libpq is already present in the runtime image via postgresql-client, so nothing extra needed there).
  • requires-python narrowed from >=3.9,<=3.11 to >=3.10,<=3.11 since pg-nearest-city needs 3.10+. Doesn't change anything for actual deploys, the Docker image already runs 3.10.
  • set_country_info is now async and queries AsyncNearestCity with the project's centroid instead of hitting Nominatim. It reads location.country_name (the full country name from the package's dataset) rather than location.country (an ISO-style code like USA), specifically to avoid breaking existing country filters/charts, which expect long names. For the UK test fixture this still resolves to "United Kingdom", matching the old Nominatim-based expectation exactly.
  • Removed OSM_NOMINATIM_SERVER_URL from backend/config.py, example.env, and the three AWS ECS env hcl files, since it's no longer read anywhere.
  • 2.0.0 specifically added country-boundary matching (ST_Covers on the country polygon before finding the nearest city within it), which is what fixes the cross-border mismatch reported in this issue's thread (a Nepal AOI near the India border resolving to the wrong country). I checked the actual query pg-nearest-city runs and confirmed it scopes the nearest-city search to the country whose polygon covers the point, so it shouldn't reintroduce the "closest city regardless of border" bug.
  • Left the on-disk PostGIS data import out of scope here. AsyncNearestCity already handles this itself: on first connection it checks for its country/geocoding tables, and if they're missing it auto-imports the bundled compressed CSVs (guarded by a Postgres advisory lock so concurrent requests don't race each other). So the first project create/update after this ships will take longer (the package's docs say 30s-4m), everything after that is fast. No separate migration or deploy step needed for that part.

Alternative Approaches Considered

The issue thread raised using ISO country codes instead of country names for the whole country field, to sidestep the Nominatim-vs-pg-nearest-city naming mismatch entirely. That's a bigger change (DB column, API responses, frontend filters all currently key off the long name), so I left it out of this PR and just picked the field (country_name) that matches the existing naming convention as closely as pg-nearest-city allows. Happy to follow up separately if that's still wanted.

I also didn't add a data-backfill migration for existing projects' country values (the repo has precedent for this, see migrations/versions/7937dae319b5_.py from 2020). Existing projects already have country set from Nominatim and this PR only affects new lookups going forward, so I don't think a backfill is required for this fix, but flagging it in case reviewers disagree.

Review Guide

  • backend/models/postgis/project.py: Project.set_country_info is the core change.
  • backend/services/project_admin_service.py and the update method in project.py: both call sites now await the method.
  • tests/api/unit/models/postgis/test_project.py: replaced the Nominatim-mocking test with one that patches AsyncNearestCity and asserts country gets set from location.country_name, plus a new test for the lookup-failure path (make sure it doesn't blow up project creation, just leaves country unset).

Testing done

  • Verified against the actual pg_nearest_city package (installed it locally, read the real source, not just the README) to confirm the class names, query() signature, and the Location dataclass fields (country vs country_name) before writing the integration.
  • uv lock resolves cleanly with the new dependency.
  • flake8 and black --check pass on all touched files.
  • Confirmed backend/models/postgis/project.py and backend/services/project_admin_service.py import cleanly with all real dependencies installed, and that set_country_info is genuinely a coroutine now.
  • Ran the new unit test logic directly against the real Project.set_country_info method (mocking AsyncNearestCity the same way the test file does) and confirmed both the success and failure paths work.
  • Did not run the full pytest suite (tests/api/unit/, tests/api/integration/) locally. It needs the docker-compose Postgres/PostGIS stack, which isn't available in my environment. Deferring to CI for that part.

Environment variable changes

Removed OSM_NOMINATIM_SERVER_URL everywhere it was referenced - backend/config.py, example.env, and the three AWS terragrunt hcl files under scripts/aws/infra/*/purgeable/. It's no longer read anywhere in the backend. If there's an infra-side reason to keep the var defined regardless, happy to revert that part.

Project.set_country_info made a blocking requests.get call to the
public Nominatim server inside an async request path. Swap it for
pg-nearest-city, which resolves the nearest city and country from
the project's own PostGIS database instead of a remote HTTP call.

set_country_info is now async and reads location.country_name to
keep the existing long-form country naming convention. Removed
OSM_NOMINATIM_SERVER_URL since it's no longer read anywhere.

Fixes hotosm#6905
@github-actions github-actions Bot added scope: infrastructure scope: backend dependencies Pull requests that update a dependency file labels Jul 5, 2026
@sonarqubecloud

sonarqubecloud Bot commented Jul 5, 2026

Copy link
Copy Markdown

@munzzyy

munzzyy commented Jul 9, 2026

Copy link
Copy Markdown
Author

Looked into the backend test failure since it's blocking on a red X.

Both run attempts fail before any test actually executes. Attempt 1 was action_required (fork approval gate). Attempt 2, after approval, fails at the docker build/push step:

#30 ERROR: failed to push ghcr.io/hotosm/tasking-manager/backend:pr-7287: denied: installation not allowed to Write organization package

That's the test-img-build / build-image job trying to push the PR's test image to GHCR so the later run-tests jobs can pull it. Both pytest-unit / run-tests and pytest-integration / run-tests then fail right after with "Failed to pull the image" - they never got a test image to run against. None of the actual pytest suite ran.

The fork PR token isn't carrying package-write to the org registry, and it's separate from anything in the diff - #7282 (also a fork PR) hit the same approval gate on attempt 1. flake8/black pass locally on the touched files per the PR description, and SonarCloud's quality gate on this PR is green.

Happy to rebase/retrigger if there's a token or workflow change on the CI side, just flagging that a plain retry won't fix this one since it's failing at the same push step both times.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file scope: backend scope: infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace usage of Nominatim with pg-nearest-city

1 participant