Handling PO Boxes and Non-Addressable Locations

This page solves one exact task: identifying addresses that are mailing destinations rather than places — post office boxes, rural routes, general delivery, military mail — and routing each to the treatment it deserves before it reaches a geocoder that will happily return a coordinate for it.

The failure this prevents is quiet and specific. A post office box geocodes to the post office, which is a real building at a real coordinate, and nothing in the result says the customer does not live there. Aggregate a few thousand of those and one census block group acquires a phantom population cluster that shows up in every demographic join and every trade-area estimate that follows.

Prerequisites

  • Python packages: pandas and regex (or the standard re) are sufficient; usaddress if you are detecting during parsing, which is the recommended point.
  • Parsed address components from parsing and standardizing US store addresses — detection on raw strings works but misses the cases where the box designator sits in a unit field.
  • A decision per use case. This is the part that cannot be automated: what a box means for a store record, a customer record and a delivery record are three different answers.

Configuration and execution parameters

Parameter Value for this task Notes
detect_at parse time Before geocoding, always
box_patterns PO BOX, POB, BOX n Matched on normalized components, not raw text
route_patterns RR, HC, RURAL ROUTE Rural and highway contract routes
military_patterns APO, FPO, DPO Overseas military mail, no ground location
general_delivery GENERAL DELIVERY Held at a post office counter
store_policy reject A store cannot have a box as its location
customer_policy degrade to postal Usable for market-level aggregates only
delivery_policy keep, flag The box is the correct delivery destination

The three policy rows are the whole point of the page. A single global rule — drop them, or geocode them anyway — is wrong for at least one of the three cases, and the cost of being wrong differs: dropping customer boxes understates rural markets, while geocoding store boxes puts a storefront in the wrong town.

Two thousand boxes become one phantom cluster In a rural county, 2,140 customer records carry post office box addresses. Geocoded naively they all land on four post office buildings, producing block groups with implausible customer densities and leaving the surrounding area apparently empty. Treated as postal-resolution records they spread correctly across the postal areas they represent. The coordinate is real; the conclusion is not geocoded as written four post offices hold 2,140 customers the rest of the county reads as empty treated as postal resolution spread across the postal areas they serve no site-level claim is made about any of them Circle size on the left is the number of records geocoded to that building.

Annotated implementation

Detection is a small amount of pattern matching applied to normalized components, plus a policy table that turns the detection into a decision. Keeping the two separate means the patterns can be improved without renegotiating the policy.

python
from __future__ import annotations

import re
from enum import Enum

import pandas as pd


class AddressKind(str, Enum):
    STREET = "street"                # a physical, addressable location
    PO_BOX = "po_box"                # mail held at a post office
    RURAL_ROUTE = "rural_route"      # a delivery route, not a point
    MILITARY = "military"            # APO/FPO/DPO — no domestic ground location
    GENERAL_DELIVERY = "general_delivery"


PATTERNS = [
    (AddressKind.PO_BOX, re.compile(r"\b(P\s*O\s*BOX|POB|POST\s+OFFICE\s+BOX|BOX)\s+\d+", re.I)),
    (AddressKind.RURAL_ROUTE, re.compile(r"\b(RR|R\s*R|RURAL\s+ROUTE|HC|HIGHWAY\s+CONTRACT)\s*\d+", re.I)),
    (AddressKind.MILITARY, re.compile(r"\b(APO|FPO|DPO)\b", re.I)),
    (AddressKind.GENERAL_DELIVERY, re.compile(r"\bGENERAL\s+DELIVERY\b", re.I)),
]

# What each use case does with each kind. The only entries that matter are the
# ones that are NOT "street" — everything else follows the normal path.
POLICY = {
    ("store", AddressKind.PO_BOX): "reject",
    ("store", AddressKind.RURAL_ROUTE): "reject",
    ("store", AddressKind.MILITARY): "reject",
    ("store", AddressKind.GENERAL_DELIVERY): "reject",
    ("customer", AddressKind.PO_BOX): "postal_resolution",
    ("customer", AddressKind.RURAL_ROUTE): "postal_resolution",
    ("customer", AddressKind.MILITARY): "exclude_from_geography",
    ("customer", AddressKind.GENERAL_DELIVERY): "postal_resolution",
    ("delivery", AddressKind.PO_BOX): "keep_flagged",
    ("delivery", AddressKind.RURAL_ROUTE): "keep_flagged",
}


def classify(components: dict) -> AddressKind:
    """Check the street line AND the unit field — boxes hide in both."""
    haystack = " ".join(str(components.get(k, "")) for k in
                        ("street", "unit", "number")).strip()
    for kind, pattern in PATTERNS:
        if pattern.search(haystack):
            return kind
    return AddressKind.STREET


def apply_policy(df: pd.DataFrame, use_case: str) -> pd.DataFrame:
    out = df.copy()
    out["address_kind"] = [classify(c or {}) for c in out["parsed"]]
    out["policy"] = [
        POLICY.get((use_case, k), "normal") for k in out["address_kind"]
    ]
    # Only STREET addresses proceed to a site-level geocode.
    out["geocode_site_level"] = out["policy"].isin({"normal"})
    return out

The detail that catches people is checking the unit field as well as the street line. A record whose street is a real street and whose unit is BOX 214 is a mailing address at that street’s post office, and a detector that only reads the street line will pass it through as a rooftop-quality record.

Failure modes and debugging

A street genuinely named Box. Streets called Box Canyon Road and businesses at Boxwood Lane exist, and a loose pattern turns them into post office boxes. Anchoring the pattern on a following number and requiring the token to be at the start of the component removes most false positives; the remainder show up as a small, stable list that can be allow-listed by hand.

Dual addresses. Many rural businesses have both a physical address and a box, and some source systems concatenate them into one field. The record is not a box — it is a street address with a box attached — so the detector should extract the street portion rather than rejecting the record. This is the case where detecting during parsing rather than on the raw string pays off, because the parser has already separated the components.

Military addresses treated as domestic. An overseas military address carries a domestic-looking postal code, so it geocodes to a location in the United States that has nothing to do with where the person is. Excluding them from geography entirely, rather than degrading them to postal resolution, is the only correct treatment.

Silent policy drift. A new use case appears — a marketing extract, say — and inherits whichever policy the code happened to default to. Making the use case an explicit argument with no default, as above, forces the decision to be made rather than inherited.

Where the non-addressable records actually are Post office boxes and rural routes account for 0.4 per cent of customer records in dense urban markets, 1.9 in suburban, 7.6 in small-town and 21.4 in rural markets. A global exclusion policy therefore removes a fifth of the customer base in exactly the markets where every record counts. A global "drop the boxes" rule is a rural data-loss policy urban core 0.4% suburban 1.9% small town 7.6% rural 21.4% — one customer in five Degrading these records to postal resolution keeps them in the market-level analysis, where they are perfectly valid, while keeping them out of anything measured at street scale. Share of customer records classified as non-addressable, by market density band.

Verification

  • Count by kind, per source, per import. A source whose box rate jumps overnight has changed a field mapping, not acquired rural customers.
  • Confirm no rejected record reached the geocoder. The assertion is trivial and it is the one that matters: a rejected store address that was geocoded anyway will be indistinguishable from a real store downstream.
  • Spot-check the false positives. Pull every record classified as a box whose street component also looks like a real street, and read them. The list should be short and stable; a growing list means the patterns need anchoring.
  • Compare market totals with and without degraded records. In rural markets the difference is large, and knowing its size is what allows a planner to interpret a coverage figure honestly.
One classification, three destinations A detected post office box is rejected outright in a store record, degraded to postal resolution in a customer record, and kept with a flag in a delivery record. The same detection drives three policies, and each is recorded on the row so a downstream consumer can see which applied. Same detection, three defensible answers detected: PO BOX 214 store record reject · a store cannot be located at a box customer record degrade to postal tier · counts in the market, not the site delivery record keep and flag · the box is the correct destination The policy that applied is written to the row, so a later consumer never has to re-derive it — or, worse, apply a different one to the same records without anyone noticing.

Frequently Asked Questions

Can a post office box be converted into a usable location?

Only to the resolution of the postal area it belongs to, which is exactly what degrading it means. Some vendors offer box-to-street matching for businesses, and where a business genuinely has both addresses on file that link is worth using; inferring one from the other is not. The honest treatment is to accept that the record tells you a market and not a place.

What about addresses that are real but not routable — new developments, private estates?

They are a different category and deserve their own flag. The address exists and geocodes correctly; what fails is the next stage, where the routing engine cannot snap it to a network that does not yet include the road. Detecting that belongs in the network topology check rather than here, but the two flags should live side by side so a record’s usability can be read in one place.

Should non-addressable records be excluded from data-quality metrics?

They should be reported separately rather than excluded. A geocoding success rate that quietly omits a fifth of rural records is a flattering number, and the honest version — success rate among addressable records, with the non-addressable share stated — takes one extra column and prevents a slow drift into believing the coverage is better than it is.

How do international equivalents differ?

Every postal system has its own non-addressable forms, and the patterns do not transfer. Poste restante, care-of addresses, box-only postal codes and delivery-point identifiers all behave like the cases here and look nothing like them textually. Keeping the pattern set per country, selected by an explicit country field, is the same discipline that address parsing needs and for the same reason.

How should the flag be surfaced to analysts using the data?

As a column with a small, closed set of values rather than as a filtered dataset. Handing analysts a pre-filtered extract makes the exclusion invisible and guarantees that two analyses of “the same” data disagree; handing them a flagged column lets each analysis apply the rule its question needs, and makes the applied rule visible in the code. Pair it with a documented default in the query layer — a view that excludes non-addressable records for site-level work — so the common case is right without removing the ability to ask the other question.

Do these classifications belong in the store’s system of record?

Yes, written back where the source system can see them. A classification that lives only in the analytical pipeline has to be re-derived every import and never improves the data anyone else uses. Writing the flag back — or at minimum reporting it to the team that owns the source — turns a recurring cleanup into a fix, and the box that was entered as a store address gets corrected once rather than reclassified monthly.

← Back to Geocoding & Address Normalization