TL;DR: Standardisation is a dictionary lookup plus three rules — uppercase the identifier without retyping it, keep multiple designators in written order, and allow the handful of designators that legitimately carry no identifier. The table and the code are below; the extraction step that feeds them is covered in parsing unit and secondary designators.
The Mapping Table
Publication 28 defines a fixed vocabulary of secondary designators and their approved abbreviations. The mapping is many-to-one — several spoken and written forms collapse onto one abbreviation — and it is stable, which is what makes a plain dictionary the right implementation rather than anything cleverer.
| Approved | Accepts | Identifier required |
|---|---|---|
APT |
Apartment, Apt, Apt., Ap | yes |
STE |
Suite, Ste, Ste., Su | yes |
UNIT |
Unit, U, bare # |
yes |
BLDG |
Building, Bldg, Bld | yes |
FL |
Floor, Flr, Fl | yes |
RM |
Room, Rm | yes |
DEPT |
Department, Dept | yes |
SPC |
Space, Spc | yes |
TRLR |
Trailer, Trlr | yes |
LOT |
Lot | yes |
SLIP |
Slip | yes |
PIER |
Pier | yes |
HNGR |
Hangar, Hngr | yes |
BSMT |
Basement, Bsmt | no |
LBBY |
Lobby, Lbby | no |
FRNT |
Front, Frnt | no |
REAR |
Rear | no |
PH |
Penthouse, PH | optional |
UPPR |
Upper, Uppr | no |
LOWR |
Lower, Lowr | no |
The right-hand column is the part teams usually miss, and it drives a real validation rule.
BSMT and LBBY identify a delivery point on their own; a parser that requires an
identifier after every designator will reject them as malformed and route perfectly valid
addresses into a review queue.
Three Rules That Do the Work
Standardisation looks like a lookup and is actually a lookup plus three decisions that each destroy data if made carelessly. All three come down to the same principle: change the representation, never the content.
The leading-zero case in the first panel is worth dwelling on because it survives casual
testing. Buildings that number units 01 through 48 are common, and a pipeline that
passes identifiers through an integer column silently rewrites 04 as 4. Both look
plausible; only one matches the building’s own signage and the carrier’s records.
The Normalizer
from __future__ import annotations
from dataclasses import dataclass
DESIGNATOR_MAP: dict[str, str] = {
"APARTMENT": "APT", "APT": "APT", "AP": "APT",
"SUITE": "STE", "STE": "STE", "SU": "STE",
"UNIT": "UNIT", "U": "UNIT",
"BUILDING": "BLDG", "BLDG": "BLDG", "BLD": "BLDG",
"FLOOR": "FL", "FLR": "FL", "FL": "FL",
"ROOM": "RM", "RM": "RM",
"DEPARTMENT": "DEPT", "DEPT": "DEPT",
"SPACE": "SPC", "SPC": "SPC",
"TRAILER": "TRLR", "TRLR": "TRLR",
"LOT": "LOT", "SLIP": "SLIP", "PIER": "PIER",
"HANGAR": "HNGR", "HNGR": "HNGR",
"BASEMENT": "BSMT", "BSMT": "BSMT",
"LOBBY": "LBBY", "LBBY": "LBBY",
"FRONT": "FRNT", "FRNT": "FRNT",
"REAR": "REAR",
"PENTHOUSE": "PH", "PH": "PH",
"UPPER": "UPPR", "UPPR": "UPPR",
"LOWER": "LOWR", "LOWR": "LOWR",
}
# Designators that identify a delivery point without an identifier.
STANDALONE: frozenset[str] = frozenset(
{"BSMT", "LBBY", "FRNT", "REAR", "UPPR", "LOWR", "PH"}
)
@dataclass(frozen=True)
class Designator:
abbreviation: str
identifier: str | None
def render(self) -> str:
return self.abbreviation if self.identifier is None else (
f"{self.abbreviation} {self.identifier}"
)
class UnknownDesignator(ValueError):
"""Raised for a designator token absent from the mapping."""
def normalise(raw_word: str, raw_ident: str | None) -> Designator:
"""Standardise one designator/identifier pair to Publication 28 form."""
key = raw_word.strip().upper().rstrip(".")
try:
abbreviation = DESIGNATOR_MAP[key]
except KeyError as exc:
raise UnknownDesignator(raw_word) from exc
ident = (raw_ident or "").strip().upper().strip("#:-") or None
if ident is None and abbreviation not in STANDALONE:
raise ValueError(f"{abbreviation} requires an identifier")
return Designator(abbreviation, ident)
Raising on an unknown designator rather than passing it through is the right default for a batch pipeline. An unrecognised token is either a new variant worth adding to the map or a parsing error worth investigating, and both are things you want to hear about — a silent pass-through turns them into a slow accumulation of non-standard values nobody notices.
Handling Multiple Designators
Campus and industrial addresses routinely carry two designators, and their order is meaningful: it narrows from the largest container to the delivery point. Model the secondary portion as an ordered list rather than a single pair, and the case stops being special.
def normalise_all(pairs: list[tuple[str, str | None]]) -> list[Designator]:
"""Standardise a sequence of designator pairs, preserving written order."""
return [normalise(word, ident) for word, ident in pairs]
def render_secondary(designators: list[Designator]) -> str:
"""Render the standardised secondary line for display or a label."""
return " ".join(d.render() for d in designators)
assert render_secondary(
normalise_all([("Building", "C"), ("Suite", "210")])
) == "BLDG C STE 210"
Storing the list rather than a flattened string is what keeps later questions answerable. A tenant search by building, a report grouped by floor, and a delivery label all read the same structure without re-parsing, and adding a third designator changes no schema.
Auditing an Existing Column
Most teams adopt this mapping on data that already exists, and the first useful exercise is to find out what is actually in the column today. A frequency count of distinct designator tokens usually produces a short head and a long, revealing tail: three or four dominant forms, then a scattering of typos, partner-specific conventions and values that are not designators at all.
Run that count before editing the map. Tokens appearing in more than a handful of records are worth an explicit mapping entry; tokens appearing once or twice are usually corrupt input better served by the review queue than by an ever-growing dictionary. The distinction matters because every entry added to the map is a decision you are committing to maintain, and a map that has absorbed every one-off typo becomes impossible to reason about.
The audit also surfaces the reverse problem: designators that are correct, common, and
absent from your map because nobody in the original market used them. Marina and airfield
addresses bring SLIP, PIER and HNGR; agricultural and mobile-home parks bring LOT
and TRLR. None of these are exotic, and all of them are silently rejected by a map built
from a purely urban sample.
Keep the audit query in the repository next to the map itself and run it after each significant new data source is onboarded. A partner whose export uses a designator vocabulary you have never seen shows up immediately as a spike in unknown tokens, which is far easier to act on than a slow rise in review-queue volume with no obvious cause.
Edge Cases and Failure Modes
A designator that is also a street type. PIER and LOT appear in street names as
well as in secondary designators. Because standardisation runs after extraction, and
extraction is anchored to the end of the delivery line, the ambiguity is already resolved
before this code sees the token — which is the main reason for keeping the two steps
separate.
Non-ASCII identifiers. European and Latin American units carry letters outside ASCII
(2ºD, 1ºesq). Uppercasing is Unicode-aware in Python, so .upper() handles them
correctly, but the resulting value should not be forced through an ASCII fold — that turns
º into nothing and loses the distinction between 2D and 2ºD. Apply the
character normalisation rules
for the match key only, and keep the display value intact.
Penthouse without a number. PH appears both as a bare designator and as PH2. The
STANDALONE set marks it optional, which is the only entry in the table needing that
treatment and the reason the set exists rather than a boolean column.
A designator arriving already standardised. Idempotence matters: normalise("STE", "400") must return the same value as normalise("Suite", "400"). The map contains every
approved abbreviation as a key onto itself for exactly this reason, so re-running the
normaliser over already-clean data is a no-op rather than an error.
The Audit Query, Drawn
A frequency count of designator tokens produces a characteristic shape, and recognising it is what turns the audit into a decision rather than a list.
Integration Note
The output of this step feeds two places with different needs. The rendered string goes onto labels and into the address shown back to a customer; the structured list goes into the delivery-point key and into any building-level grouping. Keeping both, rather than regenerating one from the other on demand, is what lets CASS validation submit a correctly formatted secondary line while your own analytics still group by building.
Related
- Parsing Unit and Secondary Designators — the full workflow this step belongs to.
- Extracting Apartment and Suite Numbers With Regex — the extractor that produces the pairs normalised here.
- Handling Missing Unit Numbers in Delivery Validation — what to do when the standardised unit is rejected.