Skip to content

addr validation v2#42

Merged
divazbozz merged 10 commits intomainfrom
claude/ecstatic-saha-4e00b1
Apr 20, 2026
Merged

addr validation v2#42
divazbozz merged 10 commits intomainfrom
claude/ecstatic-saha-4e00b1

Conversation

@divazbozz
Copy link
Copy Markdown
Contributor

@divazbozz divazbozz commented Apr 17, 2026

Summary

much better address validation interface
unify battery + energyonly into a single component

  • Upgrades the address entry component with a richer validation pipeline and a kind-aware confirm modal. Targets our 7.1% L7D address-confirmation SLA miss by surfacing signals the frontend previously ignored, while preserving override for legit edge cases (rural, new-build, meter-per-structure properties).

What changed

Validation classifier (src/address-search/addressValidation.ts)

  • New AddressValidationKind: accept | missing_subpremise | confirm_subpremise | confirm_street_number | confirm_components | block
  • DPV=Y short-circuit — USPS DPV confirmed = silent submit, regardless of any Google-layer flags
  • Exposes dpvConfirmation, dpvFootnote, validatedLocality for UX + logging
  • unconfirmedFields maps Google component types (locality, route, postal_code, administrative_area_level_1) to form-field names for amber highlighting

City backfill for CDPs (src/address-search/utils.ts, flow components)

  • Dropped administrative_area_level_2 from the city fallback chain — Places Autocomplete omits locality for Census Designated Places like Cypress/Santa Fe/Pine Island, so the old fallback silently leaked county names ("Harris County") into the city field.
  • Backfill from Validation API's locality when Places doesn't return one.

Flow merge (new src/address-search/AddressSearchFlow.tsx)

  • BatteryAddressSearchFlow + EnergyOnlyAddressEntryFlow (~95% duplicated, ~320 combined LOC) merged into a single AddressSearchFlow. Parent supplies variant-specific placeholder.
  • Autocomplete input value lifted to AddressSearchApp so modal edits sync back into the input display (previously stale until redirect).

Confirm modal (src/address-search/modal/AddressConfirmModal.tsx + .copy.ts sibling)

  • Per-kind title / banner / CTA; amber field highlighting for unconfirmed components
  • Equal-weight "This is a single-family home" escape for missing_subpremise
  • block kind uses red tone but stays overridable (no dead-end)
  • Copy extracted to a sibling AddressConfirmModal.copy.ts — kind → partial override shallow-merged onto DEFAULT_COPY; confirm_components banner dynamically names the unconfirmed component(s) ("this city" / "the street name and ZIP code")

Observability
PostHog events fire at every fork: address_validation_result (silent or modal shown), address_validation_override (Confirm / SFH / edited), address_validation_dismiss. Each event carries raw validation signals for queryable analysis.

Design doc (docs/address-entry-design-principles.md)
7 principles: submit in one motion; warnings answer what we saw / why it matters / what to do; override always one click away; defer to the strongest signal; one modal kind-aware; observable; rural / new-build / meter-per-structure first-class.

Why

L7D (TX, non-homebuilder): 92.9% confirmed vs 95% SLA. 759 failures broken down:

Bucket Count Real nature
Unconfirmed street_number 502 (66%) Real addresses from Places; USPS DPV blind spot for rural routes + new construction. HAS_ESID rate 1.6%.
Unconfirmed subpremise 108 (14%) Mix of real multi-units with wrong unit# and meter-level values (barns, guest houses, trailers)
Missing subpremise 90 (12%) 7/8 sampled are SFHs Google mis-flagged as multi-unit
Unconfirmed locality/postal/route/country 36 (5%) CDP city names + long-tail
Unconfirmed point_of_interest 25 (3%) Separate URL-pollution bug (out of scope)

The old frontend only prompted when possibleNextAction === CONFIRM_ADD_SUBPREMISES. The backend then marked the rest as UNCONFIRMED without the user ever seeing a warning. This PR closes that gap.

Test plan

  • Typecheck clean (tsc --noEmit)
  • Lint clean (biome)
  • Build succeeds (Vite)
  • Walked through each modal kind in the browser (see Testing below)
  • Verified DPV=Y short-circuit against a 40-address probe: 0 mis-accepts, 0 degradations
  • Verified component→field mapping for locality / route / postal_code / administrative_area_level_1

Testing

  • missing sub premise
image
  • unconfirmed sub premise
image
  • unconfirmed street number
image
  • unconfirmed street
image
  • unconfirmed city
image
  • unconfirmed zip
image
Open with Devin

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Important

Review skipped

Auto reviews are disabled on this repository. To trigger a review, include rabbit in the PR description. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0733ddf1-43e1-451e-85bb-9a78847f78ad

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/ecstatic-saha-4e00b1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

divazbozz and others added 5 commits April 19, 2026 14:54
- missing_subpremise / confirm_subpremise / confirm_street_number / confirm_components now use the terser, action-oriented copy
- confirm_components banner dynamically names the unconfirmed component (street name, city, state, ZIP code) with proper singular/plural form

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- parseAddress / parseGoogleAddressComponents accept { cityFallback } and
  apply it when Places omits a locality (e.g. CDPs like Cypress, TX)
- shared resolveCity + indexByType helpers between the two parsers
- useAddressAutocomplete.resolveSelection now returns the raw Place instead
  of pre-parsed shapes — lets the flow parse after validation completes so
  the fallback is available
- AddressSearchFlow parses with validation's locality in one shot; removes
  the inline patch blocks on resolved.selection.address.city and
  resolved.googleAddressComponents.city

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- drop unused requiresSubpremise convenience flag (Issue 5)
- collapse 5 parallel edited* booleans into a single editedFields: string[]
  emitted on address_validation_override (Issue 6). Centralized diff logic
  in a diffFields helper shared with handleContinue.
- extract <ConfirmField> component from AddressConfirmModal; 5 inputs
  become 5 one-liners with warn/error visual states encapsulated (Issue 7)
- split validateAddress into fetchValidation + interpretValidation so the
  classifier is testable without mocking fetch (Issue 8)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Typing "938 Ramblewood" without debouncing fires ~13 API requests (one
per keystroke). Add a 200ms debounce before the searchQuery drives the
fetch — the controlled input keeps updating immediately; only the fetch
lags. Saves Places API quota + client bandwidth with no perceptible
suggestion lag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue 1 — classify() now only returns confirm_components when at least one
unconfirmed type maps to a form field. Prevents the modal from rendering
"the highlighted fields" with nothing actually highlighted (e.g. when
unconfirmedComponentTypes contains only point_of_interest or country).
Defensive fallback in joinLabels updated to "this address" for safety.

Issue 2 — modal's address_validation_result PostHog event now carries
confirmation_path: "modal" so analytics can distinguish silent submits
from modal-shown paths.

Issue 3 — edit detection (diffFields) now compares against a snapshot of
the form's initial values captured at modal mount, not against
googleAddressComponents directly. The form pre-fills with a fallback
(selection.address.X) when Google's component is empty, so the old
comparison falsely reported user_action="edited" when the user actually
confirmed as-is.

Issue 4 — clarified the AddressValidationKind docstring for `block`. The
intent is "warn aggressively but still allow override" per design
principle #3 (override always one click away). Updated the comment to
match observed behavior — Continue still submits, backend treats as
confirmed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@divazbozz divazbozz merged commit 1b5aa3d into main Apr 20, 2026
2 checks passed
@divazbozz divazbozz deleted the claude/ecstatic-saha-4e00b1 branch April 20, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants