# 7. Postcode search creates Properties directly, with OS-derived overrides

Date: 2026-07-06

## Status

Accepted

Numbering note: 0003 is taken by the app-authored-scenarios ADR (PR #353);
0004–0006 are referenced by CONTEXT.md but were lost to a `docs/adr/**`
.gitignore rule (removed in this commit). 0007 avoids all collisions.

## Context

Properties previously came into existence only via the BulkUpload finaliser or
the backend pipeline. The "Postcode search" journey (CONTEXT.md) lets a user
search a postcode (postcodes.io for validation + geography, OS Places for
addresses) and add selected addresses to a Portfolio directly — before any
modelling exists.

## Decision

- **Next.js inserts `property` rows directly**: address, postcode, uprn,
  per-property lat/lng, local_authority (postcodes.io `admin_district`),
  constituency (postcodes.io `parliamentary_constituency`). Nothing inferred —
  no type/age/tenure guesses on the row. Energy state is derived: no EPC-graph
  rows ⇒ "awaiting modelling" (mirrors ADR on app-authored scenarios).
- Dedup rides the existing partial unique index `uq_property_portfolio_uprn`
  via `ON CONFLICT DO NOTHING`; the submit reports added vs already-existed.
- **Both override layers are written**, mimicking the finaliser: a
  VocabularyMapping upsert per distinct OS classification code and a
  `property_overrides` snapshot per property (original description = the OS
  code). New `override_source` enum value **`os_places`** — the deterministic
  OS-code mapping is not the LLM "classifier" and provenance must stay
  distinguishable.
- **Deterministic mapping** (fixed table, tested): RD02→House+Detached,
  RD03→House+Semi-Detached, RD04→House (Mid/End unknowable — no built form),
  RD06→Flat, RD07→House, RH01/RH02/RH03→House (the dedicated HMO family, built
  form unknowable). Every other code maps nothing; `Unknown` is never
  written (absence, not noise) — preserving the finaliser's invariant. The
  backend's predict-EPC service consumes these for archetype matching.
- **Addable codes**: the RD (dwelling) family except RD10 (institutions), plus
  the RH (HMO) family. All other codes — commercial, institutional, land — are
  not addable and render "Non-residential".
- **Read-through cache** on the existing `postcode_search` jsonb table:
  serve when fresher than 30 days, else refresh from OS Places and upsert.

## Alternatives considered

- Routing through the BulkUpload pipeline (synthesise a one-postcode "upload"):
  full provenance for free, but drags an interactive journey through an async
  multi-stage state machine built for files.
- Property row carries property_type directly: bypasses the override layers the
  Model backend actually consumes, and creates a second source of truth.

## Consequences

- A third Property creator exists; anything assuming "property ⇒ came from a
  BulkUpload or the backend" is now wrong.
- `property_overrides` gains a second writer with `os_places` provenance;
  re-classification tooling must not clobber user rows (existing rule) nor
  treat OS rows as user-authored.
- Postcode-search-created properties render blank/awaiting-modelling in the
  portfolio table and reporting until a scenario is modelled against them.
