TL;DR: A validator that reports “building found, unit missing” is telling you something far more useful than “invalid address” — it identifies the single field the customer needs to supply. Model the unit as a three-state field (confirmed, missing, unconfirmed), prompt only for what is missing, and retry unconfirmed units after the next reference-data refresh rather than asking the customer twice. The parent workflow is parsing unit and secondary designators.
Three Outcomes, Not Two
Validation results for a multi-unit address are frequently collapsed into valid or invalid, and that collapse destroys the most actionable distinction in the response. A building that was found with no unit supplied is a different situation from a building that was found with a unit it does not have, and both differ from a street address that does not exist.
The middle row is where the commercial difference lives. Asking a customer to re-check an address they typed correctly is a friction point that measurably costs completions; asking for one missing field with the building already confirmed is a small, obviously reasonable request. Same underlying result, very different conversion.
Modelling the Unit State
The temptation is a boolean unit_valid. Three states are needed, because “we did not
confirm it” and “it is wrong” carry different downstream obligations — the first may
resolve itself, the second will not.
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
class UnitState(str, Enum):
CONFIRMED = "confirmed" # validator recognised this exact unit
MISSING = "missing" # building needs a unit, none supplied
UNCONFIRMED = "unconfirmed" # unit supplied, validator did not recognise it
@dataclass(frozen=True)
class ValidatedAddress:
primary_confirmed: bool
unit_state: UnitState
unit_text: str | None # exactly what the customer supplied
checked_at: str # ISO timestamp of the validation call
@property
def deliverable(self) -> bool:
"""Confident deliverability — the only state safe to automate on."""
return self.primary_confirmed and self.unit_state is UnitState.CONFIRMED
@property
def needs_prompt(self) -> bool:
return self.primary_confirmed and self.unit_state is UnitState.MISSING
Keeping unit_text alongside the state is the detail that prevents a common data loss.
When the state is UNCONFIRMED, the customer has told you something real; discarding it
because a reference file has not caught up means the courier gets an address with no unit
at all, which is strictly worse than an unverified one.
Turning the State Into a Prompt
The prompt should ask for exactly one thing and should show the customer that the rest of their input was accepted. That framing is not cosmetic — it changes the question from “did you get your address wrong?” to “which apartment?”, and the completion rates differ accordingly.
def prompt_for(address: ValidatedAddress, formatted_primary: str) -> dict[str, str]:
"""Build the minimal correction prompt for a validated address."""
if address.deliverable:
return {}
if address.needs_prompt:
return {
"field": "unit",
"confirmed": formatted_primary,
"message": "We found this building. Which apartment or suite?",
}
if not address.primary_confirmed:
return {
"field": "address",
"message": "We could not find this street address. Please check it.",
}
return {
"field": "unit",
"confirmed": formatted_primary,
"message": (
"We could not confirm that unit, but we will send it as entered."
),
"blocking": "false",
}
The last branch returns a non-blocking message deliberately. An unconfirmed unit on a confirmed building is usually a data-freshness problem rather than a customer error, and blocking checkout on it converts a reference-data lag into lost revenue.
Retrying, Rather Than Re-Asking
Unconfirmed units frequently become confirmed after the next postal reference update, with no change to the address at all. A scheduled re-validation of unconfirmed records clears most of them without contacting anyone.
The three-refresh cap matters. Without it, a genuinely wrong unit — a transposed digit, say — sits in the queue being re-validated indefinitely, consuming quota and never resolving. Capping the retries converts an infinite loop into a small, finite review queue.
What to Measure
Three numbers tell you whether this handling is working, and all three are cheap to emit from the validation call itself. Track them per week rather than per run, because the denominators are small enough that daily figures are mostly noise.
The first is the share of validations that return “primary confirmed, secondary missing”. This is the prompt rate, and it should be stable. A sudden rise almost always means an upstream form stopped collecting the unit field, not that customers changed behaviour — which makes it one of the more reliable early warnings of a broken integration.
The second is the conversion rate of the prompt itself: of the customers shown the one-field prompt, how many supply a unit. A healthy figure sits well above the completion rate of a generic “please check your address” message, and that gap is the entire justification for building the targeted prompt. If the gap is small, the prompt copy is probably re-presenting the whole address rather than isolating the missing field.
The third is the clearance rate of the unconfirmed queue after each reference-data refresh. A high clearance rate confirms that unconfirmed units are mostly a data-lag problem and the non-blocking policy is right. A low one suggests they are genuinely wrong units, which argues for tightening the prompt at entry rather than for retrying more often.
Together, the three convert a policy argument — should we block on an unconfirmed unit? — into a measurement, and that conversion is usually the fastest way to end a long-running disagreement between an operations team and a growth team.
Edge Cases and Failure Modes
Buildings that report a unit requirement inconsistently. Some addresses toggle between requiring and not requiring a secondary as reference data changes. Treat a record that has ever been confirmed as confirmed until the address text itself changes, rather than re-opening it every time the requirement flag flips.
Units supplied in the primary line. If the customer typed 100 Main St Apt 4 into a
single field and your form has a separate unit input, the extractor must run before
validation or the submitted primary line will contain the unit and the validator will
report a primary failure. This is the most common integration bug in checkout flows and
it presents as “valid addresses being rejected”.
Commercial addresses with internal mail stops. MS 4-320 is not a postal secondary
designator; it is an internal routing code the carrier ignores. Keep it in a separate
delivery-notes field so it reaches the recipient’s mailroom without confusing validation.
Aggressive prompting on business addresses. Many commercial buildings do not require a suite for delivery even though they have them. Prompting on every commercial record annoys customers for no gain — trust the validator’s requirement flag rather than inferring a requirement from the presence of multiple tenants.
Prompt Conversion, Measured
The commercial claim behind the one-field prompt is testable, and it is worth testing rather than asserting — the gap between the two designs is the whole justification for building the targeted version.
Integration Note
This logic sits directly on top of CASS-style validation, which supplies the codes distinguishing the three states, and directly under the deduplication key, which must decide whether an unconfirmed unit participates in the key. Our recommendation is that it does: two customers in the same building with different unconfirmed units are different records, and merging them because neither unit could be verified is a much worse error than carrying two rows.
Finally, keep a small sample of prompted-and-abandoned sessions for periodic manual reading. The aggregate numbers say whether the prompt works; the individual sessions say why it does not when it fails, and the two questions are rarely answerable from the same data.
Related
- Parsing Unit and Secondary Designators — the workflow that produces the unit this page validates.
- Normalizing Secondary Unit Designators to USPS Standards — standardising the unit before submitting it.
- Step-by-Step Guide to CASS Address Validation — the validation call and the codes it returns.