Handling Missing Unit Numbers in Delivery Validation

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.

Three outcomes and the response each deserves Three rows. When the primary address itself fails, the whole address must be re-entered. When the primary is confirmed and no secondary was supplied, the customer is prompted for the apartment or suite only. When the primary is confirmed but the supplied secondary is not recognised, the unit is kept for review and the order proceeds with a flag rather than blocking. primary not found the street address itself is wrong — re-present the whole address for correction primary confirmed · secondary missing the building exists and needs a unit — prompt for the apartment or suite field alone primary confirmed · secondary not recognised keep the supplied unit, flag it, and let the order proceed — the data may simply be behind

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.

Lifecycle of an unconfirmed unit An unconfirmed unit enters a queue. After each reference-data refresh it is re-validated. If the unit is now recognised it is promoted to confirmed and the queue entry is closed. If it fails three consecutive refreshes it is escalated for human review rather than being retried indefinitely. unconfirmed queued, order proceeds re-validate after each data refresh now recognised → confirmed close the queue entry, nobody was contacted 3 refreshes failed → review a human decides; do not retry forever

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.

Completion rate by prompt design Two bars comparing checkout completion after a validation interruption. A generic please-check-your-address message completes at a low rate. A targeted prompt showing the confirmed building and asking only for the apartment completes at a much higher rate. Completion after a validation interruption generic error "Please check your address" — whole form re-presented targeted prompt "We found 100 Main St — which apartment?" — one field, building already confirmed Measure this on your own funnel; the ratio varies by market and device but the direction is consistent.

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.