feat(address-points): store + match unit (FLAT_NUMBER) on G-NAF & OpenAddresses - #38
Merged
pdeaudney merged 1 commit intoMay 10, 2026
Conversation
…ddresses
Adds a 4-byte unit_id slot to the AddressPoint format (24 → 28 bytes), so
G-NAF FLAT_NUMBER and OpenAddresses UNIT round-trip end-to-end:
* Builder side: build_gnaf_index.rs reads FLAT_NUMBER + prefix +
suffix from ADDRESS_DETAIL columns 9/10/11; build_openaddresses_index
reads the UNIT column. Both intern the value into strings.bin and
store its id on the AddressPoint, with the 0 sentinel preserved
for the common unit-less case (~95 % of rows) so we don't burn
16M non-zero ids on empty strings.
* Runtime side: find_by_housenumber takes an optional unit_hint and
applies two-tier matching — prefer a stored record whose unit_id
matches case-insensitively, fall back to a unit-less stored record
(the building entrance) when no unit-tagged record fits. Without a
unit hint, the matcher returns the geographically-nearest record
regardless of its unit, preserving prior behaviour.
* Same discipline mirrored in the OSM addr_points fallback in lib.rs
(the OSM AddrPoint already carries unit_id from the existing
addr:unit ingestion in PR #19), so all three sources speak the
same unit-matching contract.
* /search and /validate now accept and surface unit. enrich_hit
threads the freeform AU shorthand parse (Phase 1) through to
find_addr_point_in_country and prefers the stored unit on the
matched AddressPoint over the echoed input — clients see the
authoritative G-NAF unit when a match exists, fall back to their
typed unit otherwise.
* C++ normalise_housenumber gains a digit/digit-led split for
the "3/827a" AU/NZ unit shorthand (the existing word-prefix path
handled "Apt 3/45" but missed numeric-only LHS). Restricted to
`/` separators so it can't collide with a "12-14" range.
The format break is pinned at compile time by struct_layout.rs's new
AddressPoint size assertion (28 bytes), so a future field add surfaces
as a build-time failure rather than a corrupt 13h G-NAF rebuild.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Open
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 2 of the AU unit-shorthand work (Phase 1 = #37). Widens the
AddressPointon-disk format from 24 → 28 bytes to carry an optionalunit_id, then plumbs the unit end-to-end through G-NAF + OpenAddressesresponse shapes.
FLAT_NUMBER_PREFIX | FLAT_NUMBER | FLAT_NUMBER_SUFFIXand stores the concatenated string interned in
unit_id.UNITcolumn.unit_id == 0sentinel for the unit-less common case(~95 % of rows) so we don't burn 16M non-zero ids on empty strings.
find_by_housenumber: prefer a storedrecord whose unit matches (case-insensitive), fall back to a
unit-less stored record (the building entrance) when no unit-tagged
record fits. Without a unit hint, behaviour matches prior runs.
find_addr_point_in_country(OSM'sAddrPointalready carriesunit_idfrom PR feat: close eight Nominatim ingestion gaps + remove vestigial API auth #19).enrich_hitthreads the AU shorthand parse from Phase 1 (feat(housenumber+address-points): suffix discipline, AU unit shorthand, FLAT_NUMBER storage #37)through to
find_addr_point_in_countryand prefers the storedG-NAF unit on the matched AddressPoint over the echoed input.
/validategains an explicitunitquery param.normalise_housenumberlearns the<digit>/<digit-led>split (
3/827a) so OSM ingestion records the unit when osm tagginguses the AU shorthand instead of
addr:unit.struct_layout::address_point_sizepins the new 28-byte size atcompile/test time so a future field add surfaces as a build break,
not a 13h G-NAF rebuild ending in corrupt data.
Stacking
Stacked on #37. Merging order: #37 first (Phase 1 — purely
runtime, no rebuild), then this PR (Phase 2 — binary format break,
requires a fresh G-NAF + AU OSM rebuild before serving from the new
files).
Test plan
cargo build --release(server + builders + C++ build-index)cargo test --release— 208 lib tests + ~140 integration tests pass/search?q=4+Burbank+Place+Norwestreturns theG-NAF FLAT=1 record and surfaces
unit: "1"from the storedAddressPoint (no unit in the input — proves the storage path).
/search?q=3%2F4+Burbank+Place+Norwest(AU shorthand,unit=3 in query) returns the matched record and surfaces
unit: "3"./search?q=99%2F4+Burbank+Place+Norwest(fake unit)falls back to the building, echoes input "99" — confirms
no-match path.
🤖 Generated with Claude Code