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, ILstill 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, MEnow works: only Maine has the exact aliasME→ 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.:
| metric | baseline (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. — all | 68.9% | 77.4% | +8.5pp |
| neural-only — canonical | 77.1% | 88.0% | +10.9pp |
| neural-only — perturbed | 64.8% | 72.1% | +7.3pp |
| v0-via-adapter — all | 63.7% | 70.0% | +6.3pp |
| arbiter — all | 72.0% | 80.0% | +8.0pp |
| oracle — all | 77.9% | 86.4% | +8.5pp |
| coord error p90 (km) | 1090 | 211 | 5.2× lower |
| neural resolved-but-wrong | 749 | 544 | −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.