Handle PO boxes and other edge kinds
Outcome. You can tell from a parseaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates. what shape of input you received, and you know which shapes produce a usable coordinate.
Some location strings are not streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels. addresses. StagestageOne of the dataflow stages in the runtime pipeline (normalize, locale check, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). 2.5 of the pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-hint → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package. sorts the input into one of eight kinds before the 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.' runs, and every result includes the kind. A kind check costs less than finding the same problem from a coordinate in the wrong hemisphere.
Prerequisites
- A working parseaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates., as in Install Mailwoman and parse your first address.
- A candidate 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. at
<data root>/db/wof/candidate.dbfor the geocode transcripts. Every transcript below finds it at that path, so you do not need to export a variable. - Read everything a parse returns introduces the
--debugoutput this page reads.
1. Read the kind
--debug includes the classifier's chosen kind, its confidence and the alternatives it considered:
mailwoman parse --debug "PO Box 1234, San Francisco, CA 94119" | jq -c '{path, kind}'
{
"path": "full",
"kind": {
"kind": "po_box",
"confidence": 0.95,
"alternatives": [
{ "kind": "structured_address", "confidence": 0.9 },
{ "kind": "vague", "confidence": 0.3 }
]
}
}
There are eight kinds, and the classifier always returns the whole ranked list instead of one answer:
| Kind | What it means |
|---|---|
structured_address | A conventional postal address, several segmentssegmentA punctuation-bounded chunk of the normalized input — the comma-separated parts of 'Portland, OR' — used to give downstream stages structural context. |
postcode_only | A postcodepostcodeThe country-specific postcode (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. and little else |
locality_only | A short place nametoponymA proper name for a geographic place. and no other part |
po_box | A post office boxPO boxA numbered mailbox at a post office used as a delivery address instead of a physical street location. Mailwoman tags it as the po_box component; structurally the same family as a subpremise. or its regional equivalent |
intersection | Two streetsstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels. joined by and, &, @ or at |
landmark | A venuevenueA named, non-address place — a business, building, park, or stadium. Mailwoman's free-text point-of-interest component, added as a Tier 2 fine label. name, or a relative description like behind the gas station |
poi_query | A category or brand plus a spatial anchor, when a POIpoint of interest (POI). A named place that is not strictly an address — landmark, transit stop, venue, amenity, or franchise. Mailwoman tags these as venue and resolves them through the gazetteer. lexicon is wired |
vague | The floor, always present in alternatives at 0.3 |
The top kind is a suggestion, and the pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-hint → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package. does not enforce it. vague at 0.3 is always present, so read the second
entry in alternatives for the meaningful runner-up.
2. PO boxes: they parse, they do not resolve to the box
mailwoman parse "PO Box 1234, San Francisco, CA 94119"
{
"region": "CA",
"locality": "San Francisco",
"po_box": "PO Box 1234",
"postcode": "94119"
}
The whole phrase, including the leader and the number, is one po_box component. The regional forms
follow the same convention: BP in France, Apartado in Spain, GPO Box and Locked Bag in
Australia. The tag never nests under street. It sits beside the localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighborhood in the hierarchy..
There is no PO boxPO boxA numbered mailbox at a post office used as a delivery address instead of a physical street location. Mailwoman tags it as the po_box component; structurally the same family as a subpremise. 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., and 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. would not be useful. A box is a slot in a post office, so every box at that office would share one coordinate. A geocode of a PO boxPO boxA numbered mailbox at a post office used as a delivery address instead of a physical street location. Mailwoman tags it as the po_box component; structurally the same family as a subpremise. therefore resolves whatever else is in the string:
mailwoman geocode "PO Box 1234, San Francisco, CA" | jq -c '{lat, lon, resolution_tier, locality}'
{
"lat": 37.759715,
"lon": -122.693976,
"resolution_tier": "admin",
"epistemic_status": "observed",
"locality": "San Francisco"
}
The result is San Francisco's centroid at the admin tier. That tier is the most precise one the string can support.
Treat resolution_tier: "admin" on a po_box parseaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates. as "the citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighborhood in the hierarchy. is right, but the point is not a
delivery point", and do not route a driver to it.
3. Intersections: two tags without a crossing point
mailwoman parse "5th and Main"
{
"intersection_a": "5th",
"intersection_b": "Main"
}
The parseaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates. returns one component per streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels., and the formatter joins them with an ampersand when it renders. There is no intersectionintersectionAn address that names a location by two crossing streets ('5th & Main') rather than a number and street. Mailwoman tags the two streets as intersection_a and intersection_b — a negative-space format that starved the early model. 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. either, and no step computes where the two streetsstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels. cross. The coordinate again comes from whatever place nametoponymA proper name for a geographic place. is present:
mailwoman geocode "Corner of Broadway and 42nd Street, New York, NY" | jq -c '{lat, lon, resolution_tier, locality}'
{
"lat": 40.694457,
"lon": -73.93045,
"resolution_tier": "admin",
"epistemic_status": "observed",
"locality": "New York"
}
The classifier assigns that same string a different kind:
mailwoman parse --debug "Corner of Broadway and 42nd Street, New York, NY" | jq -c '{kind: .kind.kind, confidence: .kind.confidence}'
{ "kind": "structured_address", "confidence": 0.9 }
The kind is structured_address, not intersection. The intersectionintersectionAn address that names a location by two crossing streets ('5th & Main') rather than a number and street. Mailwoman tags the two streets as intersection_a and intersection_b — a negative-space format that starved the early model. rule always scores 0.85, and
the structured-address rule scores 0.9 for any input with two or more segmentssegmentA punctuation-bounded chunk of the normalized input — the comma-separated parts of 'Portland, OR' — used to give downstream stages structural context.. A citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighborhood in the hierarchy. and a
state therefore make structured_address rank higher. Without them, the same phrase classifies as an
intersectionintersectionAn address that names a location by two crossing streets ('5th & Main') rather than a number and street. Mailwoman tags the two streets as intersection_a and intersection_b — a negative-space format that starved the early model.:
mailwoman parse --debug "5th and Main" | jq -c '{kind: .kind.kind, confidence: .kind.confidence}'
{ "kind": "intersection", "confidence": 0.85 }
The components are correct either way. If you route on the kind, check alternatives as well as the
top pick, because intersection appears there at 0.85.
4. Bare postcodes: resolvable, and ambiguous across countries
A bare postcodepostcodeThe country-specific postcode (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. is the only edge kind that resolves to its own coordinate. When the shape is unambiguous, it can also take a fast path that skips the 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.':
mailwoman parse --debug "10118-1234" | jq -c '{path, kind: .kind.kind, confidence: .kind.confidence}'
mailwoman parse --debug "10118" | jq -c '{path, kind: .kind.kind, confidence: .kind.confidence}'
{"path":"fast-path","kind":"postcode_only","confidence":1}
{"path":"full","kind":"postcode_only","confidence":0.7}
The ZIP+4 shape matches one 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.'s format, so confidence reaches 1 and the pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-hint → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package. synthesizes a
single postcode node without running the classifier. Bare 10118 matches the US, French and German
shapes at once, so confidence is 0.7 and the full pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-hint → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package. runs.
This ambiguity affects real results. Here are five bare postcodespostcodeThe country-specific postcode (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. geocoded without a 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. scope:
| Input | Resolves to |
|---|---|
20500 | 43.066, -2.492 — Spain |
10001 | 54.910, 8.311 — Germany |
02139 | 38.604, -2.166 — Spain |
75001 | 48.844, 9.367 — Germany |
SW1A 1AA | 51.501, -0.142 — United Kingdom |
Four of the five are US postcodespostcodeThe country-specific postcode (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. that a US reader would expect, and none of them resolved to the US. The alphanumeric postcodepostcodeThe country-specific postcode (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. resolved correctly, because its shape belongs to one 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..
Scope a bare postcodepostcodeThe country-specific postcode (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. by 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.. A 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 fixes some of them:
mailwoman geocode --default-country US "10001" | jq -c '{lat, lon, postcode, countryCode}'
{ "lat": 40.750634, "lon": -73.997176, "postcode": "10001", "countryCode": "US" }
That coordinate is correctly in Manhattan. A 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 does not fix every postcodepostcodeThe country-specific postcode (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one.:
mailwoman geocode --default-country US "20500" | jq -c '{lat, lon, postcode, countryCode}'
{ "lat": null, "lon": null, "postcode": "20500", "countryCode": null }
The result is null. The candidate 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. covers only part of the US postcodepostcodeThe country-specific postcode (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. set, so scoping to the right 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. turns a wrong answer into no answer. A null is the better failure, because your code can check for a null but cannot detect a wrong Spanish coordinate. Plan for it: a bare US ZIP Code is not a reliable geocodinggeocodingThe process of converting an address into geographic coordinates (latitude and longitude). Mailwoman geocodes in a multi-tier cascade: exact address-point match → street interpolation → locality centroid. Each tier is progressively coarser but more widely available. input on its own, and adding a citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighborhood in the hierarchy. makes it reliable.
5. Landmarks
mailwoman parse --debug "Empire State Building" | jq -c '{kind: .kind.kind, confidence: .kind.confidence}'
mailwoman parse "Empire State Building"
{"kind":"landmark","confidence":0.88}
{"locality":"Empire State Building"}
The kind is right, but the component is wrong. This configuration has no venuevenueA named, non-address place — a business, building, park, or stadium. Mailwoman's free-text point-of-interest component, added as a Tier 2 fine label. tier, so the name is
tagged locality and matched against place namestoponymA proper name for a geographic place.. That match finds some place with a similar name. Do not
send venuevenueA named, non-address place — a business, building, park, or stadium. Mailwoman's free-text point-of-interest component, added as a Tier 2 fine label. names to the geocoder and expect a venuevenueA named, non-address place — a business, building, park, or stadium. Mailwoman's free-text point-of-interest component, added as a Tier 2 fine label. back. Use poi_query with a POIpoint of interest (POI). A named place that is not strictly an address — landmark, transit stop, venue, amenity, or franchise. Mailwoman tags these as venue and resolves them through the gazetteer. layerlayerOne transformer block — attention plus a feed-forward network, with normalization and residual connections — applied to every position. Stacking layers lets the model build up richer representations; Mailwoman's encoder has 6. instead.
Use annotations describes what the POIpoint of interest (POI). A named place that is not strictly an address — landmark, transit stop, venue, amenity, or franchise. Mailwoman tags these as venue and resolves them through the gazetteer. layerlayerOne transformer block — attention plus a feed-forward network, with normalization and residual connections — applied to every position. Stacking layers lets the model build up richer representations; Mailwoman's encoder has 6. provides.
Verify
One command shows what shape of input you received and whether the pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-hint → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package. skipped the 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.':
mailwoman parse --debug "$ADDRESS" | jq '{path, kind: .kind.kind, confidence: .kind.confidence, alternatives: [.kind.alternatives[] | .kind]}'
{
"path": "full",
"kind": "po_box",
"confidence": 0.95,
"alternatives": ["structured_address", "vague"]
}
Limits
- The kind is mostly informational. It changes the pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-hint → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package. in exactly three places. It can skip
the 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.' for a high-confidence
postcode_onlyorlocality_only, it routes apoi_queryto the POIpoint of interest (POI). A named place that is not strictly an address — landmark, transit stop, venue, amenity, or franchise. Mailwoman tags these as venue and resolves them through the gazetteer. stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale check, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone)., and it selects the input registerinput modeThe Decision-A register switch: 'fragmented' (human-typed fragments — feeds the evidence channels) vs 'formatted' (complete records — runs the trained absence identity). Explicit on CLI/API; per-endpoint defaults (batch→formatted, autocomplete→fragmented); kind-derived otherwise.. Otherwise it is only reported on the result. po_boxandintersectionswitch the parser to the formattedinput modeThe Decision-A register switch: 'fragmented' (human-typed fragments — feeds the evidence channels) vs 'formatted' (complete records — runs the trained absence identity). Explicit on CLI/API; per-endpoint defaults (batch→formatted, autocomplete→fragmented); kind-derived otherwise. registerinput modeThe Decision-A register switch: 'fragmented' (human-typed fragments — feeds the evidence channels) vs 'formatted' (complete records — runs the trained absence identity). Explicit on CLI/API; per-endpoint defaults (batch→formatted, autocomplete→fragmented); kind-derived otherwise.. That registerinput modeThe Decision-A register switch: 'fragmented' (human-typed fragments — feeds the evidence channels) vs 'formatted' (complete records — runs the trained absence identity). Explicit on CLI/API; per-endpoint defaults (batch→formatted, autocomplete→fragmented); kind-derived otherwise. turns off 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. evidence channelsevidence channelA dedicated model input that injects externally computed per-token features (confidence-scaled, own projection) at the embedding layer: postcode anchor, gazetteer, country, street-type, locality-surface. The clue informs; the model decides (model-first). for that parseaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates.. That setting suits record-shaped input and does not suit a search box. Override it with--input-mode fragmentedif your users are typing.- Parseaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates. accuracy and geocode accuracy are separate numbers here.
po_boxscores 90.9 andintersection100 on the labeling task in the shipped evalevalRunning the model against a held-out golden dataset and computing per-component F1, exact-match, calibration, and resolved-coordinate error. ledger. Neither number measures a coordinate, because neither kind has 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.. poi_queryneeds a lexicon. The default classifier includes the POIpoint of interest (POI). A named place that is not strictly an address — landmark, transit stop, venue, amenity, or franchise. Mailwoman tags these as venue and resolves them through the gazetteer. taxonomy, but the category must be one the taxonomy contains.
Related
- Read everything a parse returns — the rest of the
--debugoutput. - Handle messy input — defects inside an address, as opposed to a different shape of input.
- Validate an address before you use it — rejecting the ones that will not resolve, before they reach a geocoder.