- JavaScript 93.1%
- HTML 2.6%
- Python 2.1%
- CSS 1.4%
- Shell 0.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| db | ||
| docs/superpowers | ||
| public | ||
| scripts | ||
| src | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
Australian Address → OSM Insert Service
A self-hosted tool to search Western Australian addresses from G-NAF and, one at a time, review and manually confirm-insert them into OpenStreetMap as new nodes.
No bulk or automated writes. Every insert is a single explicit action: you search, pick one address, see the map, the OSM tags, and the exact request that would be sent (method, URL, headers, XML body), and only then click "Confirm & insert." Nothing writes to OSM without that click.
See AGENTS.md for conventions and gotchas if you're an AI agent working
in this repo, and docs/superpowers/specs/2026-08-30-osm-address-insert-design.md
for the full design rationale.
Features
- Trigram search over ~1.5M imported WA addresses, with suburb/postcode shown alongside each result.
- Map view (Leaflet) of the G-NAF point, with existing nearby OSM address
nodes and building outlines overlaid as a duplicate-conflict warning /
existing-shape candidates (live OSM API query, always against
production regardless of
OSM_API_BASE). - A full preview of what an insert would do before you commit to it: the
OSM tags in human-readable form, and the literal
PUTrequests (URL/headers/pretty-printed XML body) that clicking confirm would send. - OSM OAuth2 login (sandbox by default), single-changeset-per-address inserts, and a "Skip" action that needs no login at all.
- Geoscape/G-NAF attribution shown in the UI footer, as required by the data's CC BY 4.0 licence.
- Optional: nearby Overture Maps building footprints (imported offline) shown in the map's "Existing shape" mode as create-a-building candidates, deduped against existing OSM buildings.
One-time setup
- Register an OAuth2 application at
https://master.apis.dev.openstreetmap.org/oauth2/applications(sandbox — do not use production until the flow is verified end-to-end). Required scopes:read_prefs write_api. The redirect URI must behttps://, exactly matchingOSM_OAUTH_REDIRECT_URIbelow — OSM rejectshttp://redirect URIs outright, with no exception forlocalhost. - Copy
.env.exampleto.envand fill inOSM_OAUTH_CLIENT_ID/OSM_OAUTH_CLIENT_SECRETfrom step 1, plusSESSION_SECRET(any long random string). If port 3000 is already in use on your machine, setHOST_PORTto a free port instead (the app always listens on 3000 inside the container regardless — updateOSM_OAUTH_REDIRECT_URIto match whichever port you choose). - Generate a self-signed TLS cert for local HTTPS (required for the OAuth
redirect above; nothing else validates the cert, so self-signed is
fine — your browser will show a one-time warning to click past):
Leavemkdir -p certs && openssl req -x509 -newkey rsa:2048 -nodes \ -keyout certs/localhost.key -out certs/localhost.crt -days 825 \ -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"TLS_CERT_PATH/TLS_KEY_PATHblank in.envinstead if you're only browsing/skipping and don't need the OSM login/insert flow yet. docker compose up -d— starts Postgres/PostGIS and the app. The app runs its database migration automatically on startup.- Load the flattened GNAF data via one of
gnaf-loader's own options — its README documents four. Two were tried in practice:- Option 1 (run
load-gnaf.pyyourself): needs both full G-NAF and the separate Geoscape Administrative Boundaries dataset (gnaf-loadercan't build GNAF without it — it depends on the boundaries data to fix addresses whose locality has no gazetted boundary). On the August 2026 release this hit a live bug: aNotNullViolationonstreets.locality_namepartway through Part 4, even with--states WAand--no-boundary-tag. It may work fine on other releases or once upstream fixes it — check https://github.com/minus34/gnaf-loader/issues first. - Option 3 (restore the maintainer's pg_dump — used here): download
the pre-built dump from
https://minus34.com/opendata/geoscape-<version>-gda2020/gnaf-<version>.dmp(only the GNAF dump is needed —address_principalshas no runtime foreign keys into the admin boundaries schema, only during the original build) and restore it directly:docker run --rm --network host -e PGPASSWORD=postgres -v <dump-dir>:/dumps:ro postgres:16 pg_restore -h localhost -p 5432 -U postgres -d addr --no-owner --no-privileges /dumps/gnaf-<version>.dmpNote: the GDA2020 dump restores into a schema namedgnaf_<version>_gda2020(with the datum suffix), not the plaingnaf_<version>thatload-gnaf.pyitself would create — pass whichever schema actually exists to the next step. This path loads all states (~15M addresses, restore takes 10-15 minutes) since the dump isn't state-filtered; Stage 2 below does the WA filtering.
- Option 1 (run
- Run the Stage 2 import:
docker compose exec app python scripts/import_gnaf.py --gnaf-schema gnaf_<version>_gda2020 --state WA - Open
https://localhost:${HOST_PORT:-3000}/and start reviewing addresses. Searching and Skipping both work with no login; logging in with OSM is only needed to actually insert.
Optional: import Overture building footprints
The map's "Existing shape" mode can also offer building outlines from a local copy of Overture Maps' buildings theme (ODbL, OSM-import-compatible). Footprints not already in OSM show as teal dashed candidates; selecting one creates a new building way from its simplified + squared outline.
Prerequisites: pip install overturemaps (in a venv or via pipx on
Debian/Ubuntu), GDAL/ogr2ogr with Parquet support (GDAL ≥ 3.5), psql.
export DATABASE_URL=postgres://postgres:postgres@localhost:5432/addr
./scripts/import_overture.sh
This downloads the WA bounding box (112.9,-35.2,129.0,-13.7) to
overture-tmp/wa_buildings.parquet (skipped if that file already exists),
loads it via a staging table, keeps single-ring polygons only, and upserts
into overture_buildings. Re-running is idempotent. Narrow OVERTURE_BBOX
if the WA-wide extract is larger than you want. On first run, check the
parquet's columns with ogrinfo -so overture-tmp/wa_buildings.parquet and
adjust scripts/import_overture.sh's SELECT if Overture has renamed one.
Reference from a WA-wide run (Overture 2025-08 release): 1,515,554 rows, ~950 MB total relation size (geometry + GIST index). The parquet download itself is ~224 MB.
Tuning knobs (all optional, defaults shown) are in .env.example under
"Overture Maps building footprints".
OSM tagging conventions used
Decided by reviewing a real WA address already in OSM (152 Elderberry Drive, South Lake) against the Australian Tagging Guidelines:
addr:street/addr:suburbare Title Case (G-NAF stores full, unabbreviated street-type words already, just upper-cased).addr:stateis omitted — only included per the guidelines when the postal state differs from the address's actual state, never true here.addr:country=AUis included;ref:gnafis not.- Changeset
sourceis"Local knowledge"andcommentis"adds address". Note this means the changeset doesn't disclose that the underlying data came from bulk G-NAF/Geoscape import — a deliberate choice, worth keeping in mind for how you use the tool. The exception is a building created from an Overture footprint: that changeset carriessource="Overture Maps"andcomment="adds address and building from Overture footprint".
Contacts (Radicale)
Optional: if you run a Radicale CardDAV server, this app can also audit your contacts' postal addresses and get them into OSM.
- Set
RADICALE_BASE_URL,RADICALE_USERNAME,RADICALE_PASSWORDin.env(HTTP Basic Auth — Radicale's normal auth model), restart. - Open
/contacts.html: pick an address book, and for each contact flagged "No address" or "Incomplete", click "Fix address" to search G-NAF (seeded from whatever address text the contact already has) and confirm the right match — this writes the confirmed address into the contact'sADRfield and links the contact to that G-NAF address. - Open
/contacts-osm-status.html: for each linked contact whose address isn't in OSM yet, click through to review and insert it, same as the normal search flow. Once inserted, come back and click "Write OSM link to contact" to add the resulting OSM node/way URL to the contact — a separate explicit step, not automatic.
Every write to Radicale requires an explicit click, same as every OSM insert. A write is rejected (shown as an error, not silently overwritten) if the contact changed elsewhere since it was last read.
Going live
Once the insert flow has been verified end-to-end against the sandbox API,
switch to production by changing OSM_API_BASE to
https://api.openstreetmap.org, OSM_WEBSITE_BASE to
https://www.openstreetmap.org, and registering a separate OAuth2
application at https://www.openstreetmap.org/oauth2/applications for the
new client id/secret. No code changes are required. (Nearby-lookup reads
already target production unconditionally via OSM_READ_API_BASE,
independent of this switch — see .env.example.)
Running tests
Backend (requires Node 20+; a TEST_DATABASE_URL Postgres/PostGIS
database with the schema applied, e.g. via docker compose up -d db):
node --test --test-concurrency=1 tests/**/*.test.js
--test-concurrency=1 is required, not optional — these are integration
tests sharing one real database, and cross-file parallelism races
TRUNCATE against concurrent inserts otherwise. npm test already
includes the flag.
Import script (requires Python 3.11+; pip install -r scripts/requirements.txt,
or use the project's .venv if present):
TEST_DATABASE_URL=postgres://postgres:postgres@localhost:5432/addr_test pytest tests/scripts/
Project structure
db/schema.sql addresses + insert_log tables, extensions, indexes
src/app.js Express app factory (dependency-injected for tests)
src/server.js runs migration, then starts HTTP or HTTPS
src/db/ data-access layer (addresses, insert_log, pool, migrate)
src/lib/ Overpass client, OSM OAuth2 client, OSM REST API client
src/routes/ search, address detail/insert/skip, auth
public/ static frontend (plain HTML/JS + Leaflet, no build step)
scripts/import_gnaf.py Stage 2 of the import pipeline (reads a gnaf-loader schema)
scripts/import_overture.sh Optional: load WA Overture building footprints into overture_buildings
tests/ node:test (backend) and pytest (import script)
docs/superpowers/ design spec and implementation plan
Data attribution
Address data is sourced from the Geocoded National Address File (G-NAF), © Geoscape Australia, licensed under CC BY 4.0.