Skip to main content

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. which shape of input you were handed, and you know which of those shapes produce a coordinate you can use and which do not.

Not every location string is a streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels. address. StagestageOne of the dataflow stages in the runtime pipeline (normalize, locale gate, 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-gate → 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 the kind is on every result. Reading it is cheaper than discovering the same thing from a coordinate that landed 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. for the geocode transcripts, at <data root>/wof/candidate.db, which is where every transcript below picks one up. Nothing to export.
  • Read everything a parse returns introduces the --debug output this page reads.

1. Read the kind

--debug carries the classifier's verdict, its confidence and what 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}]}}

Eight kinds exist, and the classifier always returns the whole ranked list rather than one answer:

KindWhat it means
structured_addressA 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_onlyA postcodepostcodeThe country-specific postal code (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_onlyA short place nametoponymA proper name for a geographic place. and nothing else
po_boxA 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
intersectionTwo 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
landmarkA named 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., or a relative description like behind the gas station
poi_queryA 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
vagueThe floor, always present in alternatives at 0.3

The verdict is a possibility, not a constraint. vague at 0.3 is unconditional, so the second entry in alternatives is the one worth reading.

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 is one po_box component, leader and number together. That is the convention, and it holds for the regional forms too: 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 there cannot be a useful one: a box is a slot in a post office, and every box at that office would share a coordinate. So 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. 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","locality":"San Francisco"}

admin tier, San Francisco's centroid. That is the correct answer to the question the string can answer. 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, the point is not a delivery point", and do not route a driver to it.

3. Intersections: two tags, no crossing point

mailwoman parse "5th and Main"
{
"intersection_a": "5th",
"intersection_b": "Main"
}

Two components, one 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.. The formatter joins them back with an ampersand when it renders. But 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 — nothing 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 — so 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","locality":"New York"}

Watchnamed watchA known below-target reading recorded at ship with an owner and a retirement condition — never a silent waiver. Example: fr.cedex shipped at 83.3 under the waived floor, named, and retired when the from-scratch base read 90.5. what the classifier does with that same string, though:

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}

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 scores a flat 0.85 regardless of evidence, and the structured-address rule scores 0.9 for anything 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. — so adding a citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighborhood in the hierarchy. and a state outranks it. Bare, 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 come out right either way, which is what matters. If you are routing on the kind, check alternatives rather than only the top pick — intersection is sitting there at 0.85.

4. Bare postcodes: resolvable, and ambiguous across countries

A postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. alone is the one edge kind that resolves to a coordinate of its own. 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.' entirely, when the shape is unambiguous:

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-gate → 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 sits at 0.7 and the full pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-gate → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package. runs.

That ambiguity is not academic. Five bare postcodespostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one., geocoded with no 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:

InputResolves to
2050043.066, -2.492 — Spain
1000154.910, 8.311 — Germany
0213938.604, -2.166 — Spain
7500148.844, 9.367 — Germany
SW1A 1AA51.501, -0.142 — United Kingdom

Four of the five are US postcodespostcodeThe country-specific postal code (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 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 postal code (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.. Doing so 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"}

Correct, Manhattan. But not all of them:

mailwoman geocode --default-country US "20500" | jq -c '{lat, lon, postcode, countryCode}'
{"lat":null,"lon":null,"postcode":"20500","countryCode":null}

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.'s US postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. 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. is partial, 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. That is the better failure — a null is checkable and a Spanish coordinate is not — but 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. Adding a citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighborhood in the hierarchy. makes it one.

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 and the component is not. There is 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 in this configuration, so the name is tagged locality and matched against place namestoponymA proper name for a geographic place. — which finds a place called something similar somewhere. 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 down the geocoder expecting 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.. poi_query and a wired 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. are the path for that; see Use annotations for 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 tells you what shape you were handed and whether the pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-gate → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package. short-circuited:

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

  • A kind is advisory almost everywhere. It changes the pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-gate → 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 short-circuit 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_only or locality_only, it routes a poi_query to 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 gate, 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 rides along on the result.
  • po_box and intersection switch 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., which turns 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). off 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 is the right call for a record-shaped input and the wrong one for a search box. Override it with --input-mode fragmented if 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_box scores 90.9 and intersection 100 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 says anything about 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_query needs a lexicon. The default classifier is wired with 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 has to be one the taxonomy knows.