Skip to content

feat(housenumber+address-points): suffix discipline, AU unit shorthand, FLAT_NUMBER storage - #37

Open
pdeaudney wants to merge 3 commits into
mainfrom
housenumber-suffix-and-unit-2026-05-10
Open

feat(housenumber+address-points): suffix discipline, AU unit shorthand, FLAT_NUMBER storage#37
pdeaudney wants to merge 3 commits into
mainfrom
housenumber-suffix-and-unit-2026-05-10

Conversation

@pdeaudney

@pdeaudney pdeaudney commented May 10, 2026

Copy link
Copy Markdown
Owner

Summary

Two-phase address-coverage work, surfaced from the 1327A Old Northern Road and 3/827a Old Northern Road test cases. Originally split as Phase 1 (#37, query-side only) and Phase 2 (#38, on-disk format change). #38 has been merged into this branch, so this PR is now the single umbrella that ships both.

Phase 1 — query-side, no rebuild needed

Suffix discipline in housenumber_matches

PR #36 was correct on the no-suffix-query → suffixed-stored axis ("42""42A") but too permissive when the query carries an explicit suffix:

Query Stored Old (PR #36) New Rationale
1327A 1327A exact
1327A 1327B distinct duplex parcels in G-NAF
1327A 1327 1327 is the original parcel; 1327A is its subdivision
1327A 1320-1340 true range; suffix quirks within span aren't decisive
1327 1327A bare query; suffix is sub-address detail

AU <unit>/<housenumber> shorthand

3/827a Old Northern Road used to lose the unit-vs-housenumber distinction at tokenisation (["3", "827a"] is identical to 3 827a). New pre-pass scans the input head with a hand-written scanner that tolerates whitespace around /, extracts unit + housenumber, and strips the prefix before tokenising.

Leading digit-LED tokens accepted as house_number

Previous rule required ALL-DIGIT leading token. 1327A Old Northern Road would lose the housenumber. New rule: any leading digit-LED token (digit + optional alpha suffix) becomes house_number when one isn't already set.

unit field on response

ParsedQuery, SearchParams, and ValidateParams gain unit: Option<String>. Echoed in the response under address.unit. Phase 2 (below) wires the actual storage match.

Phase 2 — on-disk format break, rebuild required

AddressPoint 24 → 28 bytes

New unit_id: u32 slot, pinned at compile/test time by struct_layout::address_point_size so a future field add surfaces as a build break, not corrupt data.

Builders

  • G-NAF reads FLAT_NUMBER_PREFIX | FLAT_NUMBER | FLAT_NUMBER_SUFFIX (PSV cols 9/10/11), interns the concatenated form into unit_id. 0 sentinel preserved for the unit-less common case (~95 % of rows).
  • OpenAddresses reads the UNIT column with the same 0-sentinel rule.

Two-tier matching in find_by_housenumber

When a unit hint is supplied: 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 (geographic-nearest). Same discipline mirrored in the OSM addr_points fallback in lib.rs (the OSM AddrPoint already carries unit_id from PR #19).

enrich_hit threads the unit through

The freeform AU shorthand parse is plumbed into find_addr_point_in_country. Response surfaces the stored G-NAF unit on the matched AddressPoint, falling back to the echoed input when no AddressPoint match was found.

C++ normalise_housenumber digit/digit-led split

For OSM-tagged addresses using AU shorthand (addr:housenumber=3/827a), the existing word-prefix path missed numeric-only LHS. New rule: when the separator is / (not ,/;, which would conflict with ranges) and both sides are digit-led with a digit/optional-alpha LHS, split as unit/housenumber.

Verified end-to-end

Local AU build (australia-latest.osm.pbf + G-NAF, 16,418,248 records, 459 MB points.bin = 28 B × 16M ✓):

  • /search?q=4+Burbank+Place+Norwest (no unit input) → unit: "1" from the matched G-NAF AddressPoint. Confirms storage→retrieval round-trip.
  • /search?q=3%2F4+Burbank+Place+Norwest (AU shorthand) → matches FLAT=3 record, surfaces unit: "3".
  • /search?q=99%2F4+Burbank+Place+Norwest (fake unit) → no AddressPoint match, falls back to street centroid, echoes input "99". Confirms no-match path.

Test plan

  • 14 housenumber unit tests (suffix discipline, AU shorthand parsing)
  • 5 freeform parser tests
  • 208 lib tests + ~140 integration tests pass
  • cargo build --release clean (server + builders + C++ build-index)
  • Local G-NAF rebuild (16.4M records, format size correct)
  • Manual e2e against locally-built AU index — verified above

🤖 Generated with Claude Code

pdeaudney and others added 2 commits May 10, 2026 10:11
Phase 1 of the address-coverage work — query-side only, no rebuild
needed. Phase 2 (G-NAF storage of FLAT_NUMBER + on-disk unit_id
field) ships separately.

# Changes

## 1. Suffix discipline in `housenumber_matches`

PR #36 was correct on the no-suffix-query → suffixed-stored axis
("42" matches "42A") but too permissive when the query carries an
explicit suffix:

  - `1327A` query vs `1327B` stored → was matching, NOW does not.
    These are typically distinct cadastral parcels in G-NAF
    (separate duplex units).
  - `1327A` vs `1327` (bare) → was matching, NOW does not. Stored
    `1327` is a different parcel from the suffixed subdivision.
  - `1327A` vs `1320-1340` (true range containing 1327) → still
    matches. Range form denotes a span of building numbers; suffix
    quirks within that span aren't decisive.
  - `1327` vs `1327A` → still matches (the documented permissive
    behaviour from PR #36 — bare query, suffix is sub-address
    detail the user didn't specify).

Implementation: `parse_range` becomes `parse_hn` returning
`ParsedHn { first, last, suffix }`. Suffix discipline applies before
the range-overlap check.

## 2. `parse_au_unit_address` helper

Detects the AU shorthand `<unit>/<housenumber>`:

  `3/827a`     → Some(("3", "827a"))
  `12 / 45`    → Some(("12", "45"))
  `12-14`      → None (range, not unit/hn)
  `Apt 3/45`   → None (let OSM word-prefix path handle it)
  `827a`       → None

## 3. Pre-tokenisation pass in `parse_freeform_query`

Tokenise splits on `/`, so by the time the per-token loop runs the
"/" signal is gone and we can't tell `["3","827a"]` apart from
`3 827a`. Pre-pass scans the input head for the AU pattern via a
hand-written scanner (`consume_au_unit_prefix`) that tolerates
whitespace around the slash. On match, sets `unit` + `house_number`
and strips the prefix before tokenising the rest.

## 4. Leading digit-led tokens accepted as house_number

Previous rule required ALL-DIGIT leading token to become
house_number. `1327A Old Northern Road` would tokenise to
`["1327a","old",...]` and fall into the rest bag — losing the
housenumber. New rule: any leading digit-LED token becomes
house_number when one isn't already set.

## 5. `unit` field on ParsedQuery + SearchParams

ParsedQuery gains `unit: Option<String>`, populated from the AU
shorthand. SearchParams gains `unit: Option<String>` so callers can
also pass it explicitly. Echoed back to the response under
`address.unit` via enrich_hit. Currently informational only —
G-NAF's on-disk address-point format doesn't carry FLAT_NUMBER
(Phase 2 wires that in), so the unit doesn't yet narrow the
address-point lookup.

# Verified end-to-end against AU G-NAF + OSM build

  q=3%2F827a+Old+Northern+Road+Dural → response includes
    "unit": "3"  (from AU shorthand) +
    Old Northern Road street hit
  q=1327A+Old+Northern+Road+Middle+Dural → response includes
    house_number now extracted as "1327a"

# Tests

  - 13 housenumber tests (added `suffix_discipline` covering the
    new tighter rule, `parse_au_unit_address_basic` and
    `parse_au_unit_address_rejects_non_pattern`)
  - 5 new parse_freeform_query tests covering AU shorthand,
    whitespace around slash, leading-digit-with-suffix capture,
    plain leading digit still works, and embedded slash NOT firing
  - All 208 lib tests pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…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>
…5-10

feat(address-points): store + match unit (FLAT_NUMBER) on G-NAF & OpenAddresses
@pdeaudney pdeaudney changed the title feat(housenumber): suffix discipline + AU <unit>/<hn> shorthand parsing (Phase 1) feat(housenumber+address-points): suffix discipline, AU unit shorthand, FLAT_NUMBER storage May 10, 2026
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