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.
Prerequisites
python3.11,pandas2.2.2,refrom 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:
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.
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:
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_sourcemust 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.
Related Topics
- Geocoding Quality & Address Standardization — the parent guide, covering match types and the audit record these classifications feed.
- Measuring Geocoder Match-Rate Bias in Surveillance Data — quantifying what the withheld records do to a published rate.
- Areal Interpolation & Boundary Harmonization — how to move a ZIP-level count onto tracts when an analysis needs it there.