A lightweight geocoding server
Go 100%

README.md

gazetteer #

A self-hosted, offline geocoder. Free-form address search and postal-code centroids for the United States and Canada, served from Postgres + PostGIS on hardware with plenty of disk and little RAM.

Data comes from GeoNames postal codes, OpenAddresses, and OpenStreetMap (Geofabrik extracts with replication-diff updates). The OSM import derives streets, places, address points, and landmarks: named shops, amenities, tourism, and leisure features. After import, no query ever leaves your machine.

See REQUIREMENTS.md for what it must do and DESIGN.md for how it is shaped.

Quick start #

docker compose up -d        # PostGIS for development
go build -o gazetteer .
./gazetteer migrate
./gazetteer import geonames-postal
./gazetteer import osm --region maine
./gazetteer import osm --region new-brunswick
./gazetteer serve

Downloads land in ./data (override with DATA_DIR). The database URL comes from DATABASE_URL and defaults to the compose database.

OpenAddresses downloads need an API token from an account at batch.openaddresses.io (create one on your profile page). Set OPENADDRESSES_TOKEN and the importer resolves the latest statewide output for the region and downloads it itself:

export OPENADDRESSES_TOKEN=oa....
./gazetteer import openaddresses --region maine

A file you downloaded yourself still works and skips the API:

./gazetteer import openaddresses --region maine --file data/me-statewide.geojson.gz

gazetteer update re-downloads the small sources and applies OSM replication diffs from the stored sequence cursor. When OPENADDRESSES_TOKEN is set it also refreshes OpenAddresses for every region imported from it; without the token it prints a notice and skips them. A region whose latest batch job still matches the stored cursor is skipped without a download, and its imported_at does not move, so that timestamp always means when the data last changed:

./gazetteer update            # everything
./gazetteer update osm --region maine

Endpoints #

  • GET /search?q=210+state+st+augusta+me ranked candidates as JSON, with coordinates, structured components, scores, and sources. Each result has a type: address, street, place, landmark, or postal. Landmark results add a category (the OSM tag key, like shop) and a kind (its value, like supermarket), and answer only queries without a house number.
  • GET /postal?country=US&code=04330 one postal centroid, or 404. Canadian codes resolve at full precision where the data has the code, and fall back to the three-character FSA.
  • GET /status the binary version, the schema version, row totals per source, and one entry per imported dataset with its region name, cursor, row count, and when its data last changed. The counts come from import bookkeeping, so the endpoint never counts the big tables.
  • GET /healthz liveness.

Tests #

go test ./...                    # unit tests
go test -tags integration ./...  # needs the compose database

Integration tests create their own gazetteer_test_* databases on the compose server, so they never touch imported data.

License #

MIT for the code. The imported data keeps its own licenses: GeoNames postal codes are CC BY 4.0, OpenStreetMap data is ODbL, and each OpenAddresses source has its own terms, recorded at import.