Handling PO Box and Rural Route Addresses in Case Data

A post office box is a mail destination, not a place anyone lives, and a rural route number identifies a carrier’s path rather than a parcel. Both arrive constantly in surveillance intake, both will be “matched” by most geocoders, and both produce a coordinate that is wrong in a way no downstream check can see. This guide, part of Geocoding Quality & Address Standardization, covers classifying these forms before matching and handling the cases they carry without discarding them.

Problem Context & Constraints

The failure is specific. A geocoder handed PO BOX 417, MARSHALL, MN 56258 will usually return a coordinate — the post office, or the ZIP centroid, or the place centroid — with a match type that looks respectable. Nothing about the returned record announces that the coordinate describes a building the case has never lived in. If the post office happens to sit in a different census tract from the residence, the case has been assigned to the wrong tract, and the error is systematic rather than random: every PO box in that ZIP lands at the same point.

Rural routes behave differently and are worse. RR 2 BOX 118 identifies a stop on a delivery route, and the route’s geometry is not published in any address-range file. The matcher has nothing to interpolate against, so the record either fails or falls to a place centroid. Both outcomes concentrate in exactly the sparse areas where a single misassigned case moves a rate the most.

The scale of the problem is not marginal. In many US states, postal-only addresses are the norm rather than the exception across large rural areas, and the share is far higher for older residents and for households on tribal lands — populations for whom the resulting geographic error is least acceptable.

Where a PO Box Address Actually Lands Two panels of the same three census tracts. In the first, four households with PO box addresses sit in tracts 9701 and 9702, two in each, and the post office building sits in tract 9703. In the second panel, showing what the geocoder produced, all four cases have been stacked on the post office in tract 9703, leaving tracts 9701 and 9702 with zero cases each. The result is two false zeros and one false cluster from four apparently successful matches. Four households, one coordinate, three wrong tracts where they live where the geocoder put them 9701 9702 9703 post office 9701 9702 9703 ×4 post office 2 2 0 0 0 4 cases per tract cases per tract Two false zeros and one false cluster, from a geocoder that reported four successful matches and every PO box in the ZIP lands on the same point, so the error is systematic, not random

Prerequisites

  • python 3.11, pandas 2.2.2, re from the standard library
  • The raw address strings, before any matcher call — classification must happen upstream of matching, because after matching the evidence has been replaced by a coordinate
  • A field indicating whether a separate physical or residential address was collected at intake. Many intake systems have one and never use it; finding out is usually the highest-value hour in this whole workflow
  • The reporting ZIP or county from intake, so classified records still carry a coarse geography

Before writing the classifier it is worth knowing how much of the file it will touch, and where:

Postal Share by Community Type Share of case addresses that are postal-only, by community type. Urban addresses are 2 percent postal-only, suburban 4 percent, small town 17 percent, rural 38 percent and tribal land 54 percent. The gradient means an analysis that silently drops postal-only records is dropping half of some communities and almost none of others. Postal-only share, by community type 2% urban 4% suburban 17% small town 38% rural 54% tribal land Dropping postal-only records removes half of some communities and almost none of others

Step-by-Step Solution

Classify first, route second. The classifier below is deliberately conservative: anything it is unsure about is flagged for review rather than passed through.

# Classify postal-only address forms before they reach a geocoder.
# Pinned: python 3.11, pandas==2.2.2
import logging
import re
import pandas as pd

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("geocode.postal")

# Anchored on word boundaries so "BOXWOOD LANE" and "GENERAL STORE RD" do not match.
PO_BOX = re.compile(r"\b(P\.?\s?O\.?\s?BOX|POST\s+OFFICE\s+BOX|POB)\b\s*\d+", re.I)
RURAL_ROUTE = re.compile(r"\b(R\.?\s?R\.?|RURAL\s+ROUTE|HC|HIGHWAY\s+CONTRACT)\b\s*\d+", re.I)
GENERAL_DELIVERY = re.compile(r"\bGENERAL\s+DELIVERY\b", re.I)
# A street address that ALSO carries a box is common and is usually geocodable:
# "1420 N MAPLE ST PO BOX 12" is a residence with a mailbox.
HAS_STREET_NUMBER = re.compile(r"^\s*\d+\s+\S")


def classify_postal(addr: str) -> str:
    """Return one of: street, po_box, rural_route, general_delivery, review."""
    s = (addr or "").strip()
    if not s:
        return "review"
    has_street = bool(HAS_STREET_NUMBER.match(s))
    if GENERAL_DELIVERY.search(s):
        return "general_delivery"
    if PO_BOX.search(s):
        # A leading house number means the box is supplementary, not the whole address.
        return "street" if has_street else "po_box"
    if RURAL_ROUTE.search(s):
        return "rural_route"
    if has_street:
        return "street"
    return "review"


def route_postal_records(df: pd.DataFrame, addr_col: str,
                         physical_col: str | None = None) -> pd.DataFrame:
    """Classify, then substitute a physical address where one exists.

    The substitution is the whole point: a PO box case with a recorded physical
    address is an ordinary geocodable record, and finding those first shrinks the
    problem before any modelling decision has to be made."""
    d = df.copy()
    d["postal_class"] = d[addr_col].map(classify_postal)
    d["geocode_input"] = d[addr_col]
    d["address_source"] = "mailing"

    if physical_col and physical_col in d.columns:
        has_physical = d[physical_col].notna() & (d[physical_col].str.strip() != "")
        substitute = has_physical & d["postal_class"].isin(
            ["po_box", "rural_route", "general_delivery"])
        d.loc[substitute, "geocode_input"] = d.loc[substitute, physical_col]
        d.loc[substitute, "address_source"] = "physical"
        d.loc[substitute, "postal_class"] = d.loc[substitute, physical_col].map(classify_postal)
        log.info("substituted a physical address for %d postal-only records", int(substitute.sum()))

    counts = d["postal_class"].value_counts().to_dict()
    log.info("postal classification: %s", counts)

    # Anything still postal-only must NOT go to the matcher as a residence.
    d["geocodable"] = d["postal_class"] == "street"
    log.info("%d records withheld from residence-level geocoding",
             int((~d["geocodable"]).sum()))
    return d

Records that remain postal-only after substitution are not geocoded to a point at all. They are assigned to the coarsest geography that is actually supported by the evidence — the ZIP code area, or the county — and carried forward with address_source = "mailing" so every downstream analysis can decide whether to include them.

Validation & Edge Cases

1. Confirm the classifier against a hand-labelled sample. Two hundred records read by a person is enough to estimate precision and recall for each class, and it is the only way to catch the local conventions that regex misses. Log the confusion counts:

INFO postal classification: {'street': 38104, 'po_box': 2871, 'rural_route': 806, 'general_delivery': 41, 'review': 185}
INFO substituted a physical address for 1943 postal-only records
INFO 1775 records withheld from residence-level geocoding

2. Beware the street that is also a box. RR 3 BOX 44, 1420 COUNTY ROAD 8 occurs and is geocodable. The HAS_STREET_NUMBER guard handles the common ordering; anything ambiguous goes to review rather than being guessed.

3. Check whether the physical-address field is trustworthy. Intake systems frequently carry a physical-address column that is populated by copying the mailing address. Compare the two columns: if they are identical for more than a small fraction of PO box records, the field is not a second observation and substituting from it achieves nothing.

4. Track the postal share by geography. A county where 40% of cases are postal-only cannot support tract-level analysis at all, regardless of how the remaining 60% are handled. That is a finding about the data, and it belongs in the output rather than being discovered by a reader who wonders why one county’s map looks sparse.

Four Routes Out, and Only One of Them Is a Point A postal-only record is tested for a recorded physical address. If one exists it is geocoded normally as a residence. If not, the record is assigned to the ZIP code tabulation area when the analysis is at county scale or coarser. If the analysis is at tract scale, the record is excluded from the numerator and counted in a reported exclusion tally. Only if none of those is possible is the record withheld entirely, and that outcome is logged rather than silent. Every postal-only record leaves by a named route postal-only record physical address on file? YES geocode normally source = physical NO assign to ZCTA county scale or coarser excluded from tract work counted in the exclusion tally withheld entirely logged, never silent

5. Do not let the exclusion tally live only in a log file. The count of records excluded for postal-only addressing belongs in the published metadata next to the case count, because it is the difference between the cases the programme knows about and the cases the map shows.

The output of the routing step is a ledger rather than a filtered file, and the ledger is what a reviewer checks:

The Reconciliation Every Run Must Produce A reconciliation ledger for one run. Of 42,007 submitted records, 38,104 were street addresses, 1,943 postal-only records had a physical address substituted, 1,775 were withheld from point geocoding and assigned to a ZIP area, and 185 went to manual review. The four categories sum exactly to the submitted total, and that identity is the control that catches silent loss. Four categories that must sum to the input submitted: 42,007 street addresses — geocoded normally 38,104 physical address substituted 1,943 withheld, assigned to ZIP area 1,775 manual review queue 185

Compliance Notes

  • A PO box is still an identifier. It maps to a named renter at the post office, and in a small town the mapping is public knowledge. Treat postal-only addresses with the same care as street addresses in any release, and do not assume that “it is only a box number” reduces disclosure risk.
  • Record the substitution. When a physical address replaces a mailing address, the record’s address_source must say so, because the two are collected under different assumptions and may have different accuracy and different consent context.
  • Report the postal share by stratum, since it is one of the largest drivers of the differential non-match measured in Measuring Geocoder Match-Rate Bias in Surveillance Data.
  • Never publish the post-office coordinate as a case location. If postal-only cases are mapped at all, map them as an areal count on the ZIP area, which is honest about what is known, rather than as a point, which is not.