Skip to main content

Resolver ranking: exact-match tiering + parent fallback (2026-05-30)

Direction-C resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon.-depth PR1. Two ranking fixes that attack the verified dominant failure mode of the end-to-end resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon.wrong-admin cascades from a mis-resolved regionregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality. — measured on the 2406-row WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations.-bootstrap evalevalRunning the model against a held-out golden dataset and computing per-component F1, exact-match, calibration, and resolved-coordinate error..

The bug, root-caused

The end-to-end resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. evalevalRunning the model against a held-out golden dataset and computing per-component F1, exact-match, calibration, and resolved-coordinate error. ceiling was 68.9% Acc@1Acc@1 (accuracy at 1). The share of eval rows whose top-ranked resolver candidate is the correct place, regardless of coordinate distance — 'did we pick the right place', independent of geocode precision. with a p90 coordinate error of 1090 km. Decomposing the failures pinned the cause precisely:

  • Of neural's 749 evalevalRunning the model against a held-out golden dataset and computing per-component F1, exact-match, calibration, and resolved-coordinate error. failures, 100% resolved to a place — just the wrong one (0% were unresolved). So the ceiling is a resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon.-ranking problem, not a parser-coveragecoverageThe fraction of a population or region for which a data source has real, non-placeholder entries — e.g. 47% rooftop coverage on Texas addresses. Distinct from accuracy on the rows that are present. problem.
  • 77% of the wrong resolutions are >100 km off (median wrong-error 333 km, p90 2624 km) — distances that mean wrong stateregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality., not a local near-miss.

Tracing the wrong-stateregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality. cases exposed the mechanism. For Portland, ME the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. resolved the regionregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality. "ME" to Missouri, then correctly scoped the localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. to its (wrong) parent → Portland, MO. The findPlace("ME", region, US) candidate list:

ME → Missouri score 6.83 pop 6.2M ← won
Maine score 6.04 pop 1.4M ← exact alias `ME`, but lost
Maine → Maine score 23.2 (full-name match scores ~17 higher)

Maine is the only US regionregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality. with the alias ME (verified in the gazetteergazetteerA geographical index that maps place names and postcodes to real-world coordinates. Mailwoman uses a custom-built Who's On First (WOF) SQLite database as its gazetteer — the 'atlas' half of the grammar/atlas architecture.'s names table; Missouri's alias is MO). It still lost — because the ranking adds population as a large additive boost (populationBoost, up to +4), and Missouri's larger population overcame Maine's bm25 edge on the 2-letter query.

Fix 1 — exact-match tiering (the aligned fix)

The principle (and why it aligns with population/importance rather than overriding it): population is a prominence prior. Its job is to break ties among candidates that match the query equally well — e.g. "Springfield" → Springfield IL over Springfield MA, both exact name matches, population correctly picks the bigger. It was never meant to promote a candidate that matches the query worse. The additive scheme accidentally let it do exactly that, because +4 of population exceeds the bm25 gap between an exact alias match and a non-match.

The fix makes ranking two-keyed: match quality is the primary key, prominence (population) the secondary key within a tiertierInternal versioning of which label classes the model emits. Tier 1 is the coarse components (country, region, locality, postcode); Tier 2 adds venue, street, house_number; Tier 3 (future) would add attention, po_box, and POI venue subtyping. Historically called 'Stage 1/2/3' before the runtime-pipeline naming made that ambiguous.. A candidate whose name or any alias equals the query (case-folded) ranks above any partial match; the existing weighted sum — including population — orders candidates within a tiertierInternal versioning of which label classes the model emits. Tier 1 is the coarse components (country, region, locality, postcode); Tier 2 adds venue, street, house_number; Tier 3 (future) would add attention, po_box, and POI venue subtyping. Historically called 'Stage 1/2/3' before the runtime-pipeline naming made that ambiguous..

  • Springfield, IL still works: both IL and MA Springfields are exact name matches → same tiertierInternal versioning of which label classes the model emits. Tier 1 is the coarse components (country, region, locality, postcode); Tier 2 adds venue, street, house_number; Tier 3 (future) would add attention, po_box, and POI venue subtyping. Historically called 'Stage 1/2/3' before the runtime-pipeline naming made that ambiguous. → population decides → IL (bigger). Unchanged.
  • Portland, ME now works: only Maine has the exact alias ME → higher tiertierInternal versioning of which label classes the model emits. Tier 1 is the coarse components (country, region, locality, postcode); Tier 2 adds venue, street, house_number; Tier 3 (future) would add attention, po_box, and POI venue subtyping. Historically called 'Stage 1/2/3' before the runtime-pipeline naming made that ambiguous. → population never gets to override it → Maine.

So population/importance keeps doing exactly what it was tuned for (surfacing the famous Springfield/New York/Chicago among equally-good name matches); it simply no longer leaks across match-quality tierstierInternal versioning of which label classes the model emits. Tier 1 is the coarse components (country, region, locality, postcode); Tier 2 adds venue, street, house_number; Tier 3 (future) would add attention, po_box, and POI venue subtyping. Historically called 'Stage 1/2/3' before the runtime-pipeline naming made that ambiguous.. Implemented in resolver-wof-sqlite/lookup.ts (RankingWeights.exactMatchTiering, default true, one cheap indexed lookup over <schema>.names for the over-fetched candidate ids).

Known limitation: tiering re-ranks within the over-fetched window (limit*4). An exact matchexact matchThe share of eval items whose every component is correct (compared per-span or per-token). Stricter than per-tag F1, which credits partial correctness. that falls outside that window isn't rescued. For the regionregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality.-abbrev case the window is comfortably sufficient (a handful of statesregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality. match a 2-letter query); pushing the exact-match term into the SQL ORDER BY is a follow-up if a wider case ever needs it.

Fix 2 — parent fallback

parentId is a hard descendant filter in the backend. If a parent resolves wrong, or the gazetteergazetteerA geographical index that maps place names and postcodes to real-world coordinates. Mailwoman uses a custom-built Who's On First (WOF) SQLite database as its gazetteer — the 'atlas' half of the grammar/atlas architecture. hierarchy is incomplete (a real localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. whose ancestors chain is missing its regionregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality.), the filtered lookup returns nothing and the node goes unresolved. Fix: when a parent-constrained lookup returns zero, retry once without the parent constraint (keeping the countrycountryThe top-level address component (an ISO country). Closed-vocabulary, so it is best handled by a deterministic matcher feeding a proposal rather than a retrained model head. constraint, so resolution still can't wander cross-border). Prefers a parent-scoped hit; never sacrifices recallrecallOf the spans whose gold label is a given tag, the fraction the model found. High recall means few misses. Paired with precision to compute F1.. core/resolver/resolve.ts (ResolveOpts.parentFallback, default true).

Results (2406-row WOF-bootstrap, v0.7.2 model)

One toggle apart (--exact-tiering/--parent-fallback off vs on), same modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.', same gazetteergazetteerA geographical index that maps place names and postcodes to real-world coordinates. Mailwoman uses a custom-built Who's On First (WOF) SQLite database as its gazetteer — the 'atlas' half of the grammar/atlas architecture.:

metricbaseline (off)both fixes (on)Δ
neural-only Acc@1Acc@1 (accuracy at 1). The share of eval rows whose top-ranked resolver candidate is the correct place, regardless of coordinate distance — 'did we pick the right place', independent of geocode precision. — all68.9%77.4%+8.5pp
neural-only — canonical77.1%88.0%+10.9pp
neural-only — perturbed64.8%72.1%+7.3pp
v0-via-adapter — all63.7%70.0%+6.3pp
arbiter — all72.0%80.0%+8.0pp
oracle — all77.9%86.4%+8.5pp
coord error p90 (km)10902115.2× lower
neural resolved-but-wrong749544−205 (−27%)

The headline: +8.5pp end-to-end Acc@1Acc@1 (accuracy at 1). The share of eval rows whose top-ranked resolver candidate is the correct place, regardless of coordinate distance — 'did we pick the right place', independent of geocode precision. and a 5.2× cut in p90 coordinate error (1090 km → 211 km — the cross-stateregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality. tail collapses, exactly as the "ME → Missouri" root cause predicted). Every baseline improves; the gains are largest on the canonical subset (+10.9pp) where clean two-letter regionregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality. codes are most common. Failure attribution confirms the mechanism: neural's resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon.-side errors drop from 749 to 544 (−27%) with parser-side errors still at 0 — the fix moved wrong-stateregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality. resolutions into correct ones, not coveragecoverageThe fraction of a population or region for which a data source has real, non-placeholder entries — e.g. 47% rooftop coverage on Texas addresses. Distinct from accuracy on the rows that are present..

The 211 km p90 that remains is the genuine admin-centroid tail (large rural counties / sprawling metros whose centroid is far from the labeled point) plus a residual same-stateregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality. ambiguity — the target for the learned re-ranker and the streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels.-level tiertierInternal versioning of which label classes the model emits. Tier 1 is the coarse components (country, region, locality, postcode); Tier 2 adds venue, street, house_number; Tier 3 (future) would add attention, po_box, and POI venue subtyping. Historically called 'Stage 1/2/3' before the runtime-pipeline naming made that ambiguous., not for this PR.

Reproduce

# baseline (both fixes off) vs both on — one toggle apart:
node scripts/eval/resolver-eval.ts \
--eval /tmp/wof-bootstrap/eval.jsonl --country US \
--exact-tiering {true|false} --parent-fallback {true|false} \
--model <v0.7.2.onnx> --tokenizer <v0.6.0-a0> --model-card <card> \
--wof admin-global-priority.db,postalcode-us.db

Both fixes are A/B flags (default on) so the change is one toggle from the prior behaviour and can be reverted per-call.