Skip to content

feat(address-points): store + match unit (FLAT_NUMBER) on G-NAF & OpenAddresses - #38

Merged
pdeaudney merged 1 commit into
housenumber-suffix-and-unit-2026-05-10from
address-point-unit-field-2026-05-10
May 10, 2026
Merged

feat(address-points): store + match unit (FLAT_NUMBER) on G-NAF & OpenAddresses#38
pdeaudney merged 1 commit into
housenumber-suffix-and-unit-2026-05-10from
address-point-unit-field-2026-05-10

Conversation

@pdeaudney

Copy link
Copy Markdown
Owner

Summary

Phase 2 of the AU unit-shorthand work (Phase 1 = #37). Widens the
AddressPoint on-disk format from 24 → 28 bytes to carry an optional
unit_id, then plumbs the unit end-to-end through G-NAF + OpenAddresses

  • OSM addr_points ingestion, lookup, and the /search and /validate
    response shapes.
  • G-NAF builder reads FLAT_NUMBER_PREFIX | FLAT_NUMBER | FLAT_NUMBER_SUFFIX
    and stores the concatenated string interned in unit_id.
  • OpenAddresses builder reads the UNIT column.
  • Both use the unit_id == 0 sentinel for the unit-less common case
    (~95 % of rows) so we don't burn 16M non-zero ids on empty strings.
  • Two-tier matching in find_by_housenumber: prefer a stored
    record 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.
  • Same discipline mirrored in the OSM addr_points fallback inside
    find_addr_point_in_country (OSM's AddrPoint already carries
    unit_id from PR feat: close eight Nominatim ingestion gaps + remove vestigial API auth #19).
  • enrich_hit threads 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_country and prefers the stored
    G-NAF unit on the matched AddressPoint over the echoed input.
  • /validate gains an explicit unit query param.
  • C++ normalise_housenumber learns the <digit>/<digit-led>
    split (3/827a) so OSM ingestion records the unit when osm tagging
    uses the AU shorthand instead of addr:unit.
  • struct_layout::address_point_size pins the new 28-byte size at
    compile/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
  • Local G-NAF rebuild (16,418,248 records, 459 MB points.bin = 28 B × 16M ✓)
  • Local AU OSM reverse-index rebuild
  • End-to-end: /search?q=4+Burbank+Place+Norwest returns the
    G-NAF FLAT=1 record and surfaces unit: "1" from the stored
    AddressPoint (no unit in the input — proves the storage path).
  • End-to-end: /search?q=3%2F4+Burbank+Place+Norwest (AU shorthand,
    unit=3 in query) returns the matched record and surfaces unit: "3".
  • End-to-end: /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

…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>
@pdeaudney
pdeaudney merged commit 08d2b55 into housenumber-suffix-and-unit-2026-05-10 May 10, 2026
@pdeaudney
pdeaudney deleted the address-point-unit-field-2026-05-10 branch May 10, 2026 01:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant