Merge remote-tracking branch 'origin/main' into feature/historic-epc-repository

This commit is contained in:
Khalim Conn-Kowlessar 2026-07-04 11:19:59 +00:00
commit ab245de68d
245 changed files with 27388 additions and 1658 deletions

View file

@ -16,6 +16,35 @@ Ask for **portfolio_id** and **scenario_id** if the user didn't give them.
`scenario_id` is optional — without it, each Property's *default* plan (the one
shown in the FE) is audited.
## Query safety (READ FIRST — a bad query can take the shared DB down)
The `recommendation` table is **~26m rows and has NO index on `plan_id`** (only
`id` and `property_id`). Any query that reaches `recommendation` through
`plan_id` — a `JOIN ... ON r.plan_id = pl.id`, or a correlated
`EXISTS (SELECT 1 FROM recommendation WHERE plan_id = pl.id ...)` — forces a full
seq-scan of all 26m rows. On a large portfolio (e.g. 796/1268 has ~32k default
plans) the planner picks that seq-scan **even when the query also joins
`property_id`**. This is what blocked the DB during the portfolio-796 audit.
Rules for EVERY query you write or run here (the runner already follows them):
- **Confirm with the user before running any ad-hoc SQL** that touches
`recommendation`, and show them the `EXPLAIN` plan. Never improvise a
`recommendation` query against the live DB unprompted.
- **`EXPLAIN` first** (no `ANALYZE` — it executes nothing). If the plan contains
`Seq Scan on recommendation`, do NOT run it. Rewrite, or add the index below.
- **Never reach `recommendation` via `plan_id` or a correlated subquery.** Scope
to the portfolio's properties and reach `recommendation` via the indexed
`property_id`, and prefer ONE scoped aggregate over per-property subqueries.
- The runner sets a **`statement_timeout` (120s)** on its connection as a hard
ceiling, and the `recommendation` rollup is **opt-in** (see Phase 1). Keep any
ad-hoc connection equally bounded.
- **The real fix is an index:** `CREATE INDEX CONCURRENTLY
idx_recommendation_plan_id ON recommendation (plan_id)` (non-blocking build).
Until it exists, the solar checks can't run cheaply on a large portfolio — that
is a deliberate trade-off, not a bug. Propose it as a migration if the solar
checks are needed portfolio-wide.
## Phase 1 — Build the dataset
```
@ -26,6 +55,16 @@ Writes `modelling_audit.md` (grouped, ranked by severity) and
`modelling_audit.csv` (every flagged row: property_id, uprn, severity, check,
detail). Read the printed summary and `modelling_audit.md`.
**Recommendation checks are OFF by default.** The two solar checks
(`excessive-solar-sap`, `low-solar-bill-savings`) read the `recommendation`
rollup, which is the only query that scans the 26m-row table. They are gated
behind `--with-recommendations`, which is EXPLAIN-guarded and will **abort** if
the plan seq-scans `recommendation` (raising `RecommendationScanError`). Without
the flag those two checks are inert and every other check is bounded by the
portfolio — this is the safe default. Only pass `--with-recommendations` once
`idx_recommendation_plan_id` exists (or on a small portfolio where EXPLAIN is
clean), and confirm with the user first.
## Phase 2 — Review the high-level results
For each check group, HIGH severity first: note the count, read a few example
@ -53,6 +92,13 @@ group, cluster the flagged properties by a distinguishing trait — EPC source
(lodged / predicted), `rebaseline_reason`, property type, the dominant measure,
fuel — using SQL against the DB. Report the sub-classes and their sizes.
Traits from `property` / `property_baseline_performance` / `plan` are cheap to
cluster on. **Traits that need `recommendation` (dominant measure, measure mix)
are NOT** — obey the Query-safety rules above: scope to the *flagged* property
ids only (a few hundred, so the `property_id` index is used), `EXPLAIN` first,
and confirm the query with the user. Never cluster the whole portfolio's
measures in one `recommendation` query.
## Phase 5 — Cross-reference open work
Before proposing fixes, check whether one is already in flight:
@ -114,9 +160,27 @@ durable, compounding output of every audit.
## Notes
- Read-only on the DB. `run_modelling_e2e` is a dry run.
- **Audit the default plan only** — every check, and every characterisation
query, must filter `plan.is_default = TRUE` (the FE-shown plan). The runner's
`_QUERY`/`_ROLLUP_QUERY` already do; keep ad-hoc SQL consistent or the counts
mix superseded plans into the picture.
- **The stored default plan can be STALE vs the live model** — the persisted plan
is the output of an *earlier* modelling run; `run_modelling_e2e` re-models
against current logic + live EPC/solar. A stored-vs-live gap is the single
biggest driver of MEDIUM/HIGH anomalies on a not-yet-re-modelled portfolio, and
it spans more checks than the one below names. The fix is operational
(**re-model the portfolio, then re-audit**), not a code change — confirm a
sample with `run_modelling_e2e` before debugging the calculator.
- **Expected, not bugs** (until the override-aware-rebaseline + persistence-fidelity
PR deploys and the portfolio is re-modelled): much of `zero-works-post-differs`
and `plan-score-below-baseline` is the pre-fix baseline-vs-plan divergence and
should shrink after re-model — note it, don't re-debug it.
work — see `docs/adr/` — is re-modelled into the portfolio): much of
`zero-works-post-differs`, `plan-score-below-baseline`, `already-meets-goal-with-works`,
and the non-fuel-switch slice of `negative-bill-savings` is the same stale
stored-plan-vs-live divergence and should shrink after re-model — note it,
don't re-debug it. Confirm a sample's stored plan differs from `run_modelling_e2e`
(stored `air_source_heat_pump` for tens of £k where the live plan is a cheap
profitable `solar_pv`; stored works on a property the live model leaves at £0
because it already meets goal). The `negative-bill` certs that DO carry an
`air_source_heat_pump` are a genuine gas→electric fuel-switch (bills rise at
current price ratios) — expected by design.
- Adding a check is one decorated `(PropertyAudit) -> Optional[str]` function in
`scripts/audit/anomalies.py`; see its module docstring.

View file

@ -169,6 +169,34 @@ Pattern: `with E.session() as (ctx,page): E.goto(...); E.set_text/set_select(...
lines 17/18). This drove the first campaign mapper fix — see Banked findings.
## Banked findings (fold new ones in here as the corpus grows)
- **Register cost-triangulation (no Elmhurst needed):** the register lodges
`heating_cost_current` / `hot_water_cost_current` / `lighting_cost_current`
at RETAIL prices — absolute £ never match SAP Table-12 spec prices, but for
same-fuel end-uses the RATIOS localize a divergence without a worksheet.
Normalize by lighting: `(lodged_hw/lodged_light) / (our_hw/our_light)` ≈ 1
means our water cascade agrees with the lodging software; »1 flags the
divergent end-use. CAVEAT: the register's "hot water cost" INCLUDES the
SAP (64a) electric-shower electricity — add `instant_shower` cost to our
hw bucket before comparing on any electric-shower cert, else every
shower-outlet-type-2 cert false-positives at ratio ~2 (corpus-1000 band
triage, 36 certs, mean ratio 2.0 → 1.04 after adding (64a)).
- **Lodging-software conventions diverge from Elmhurst on two known axes**
(mark such corpus certs ⚠, don't tune): (a) out-of-range-PSR database
heat pumps — SAP 10.2 N2 footnotes 44/45 mandate the reciprocal extension
toward 100% at 2× the record's largest PSR (Elmhurst does this, golden
case 56); at least one software bills 100% direct electric + standard
schedule instead (corpus 4510053280, lodged 51 vs engine/Elmhurst-method
75). (b) a lodged 0 extract fans — Elmhurst renders 0 as "unknown" →
Table 5 age-band default (worksheet case 46); LIG-21.0 lodgements
(calculation_software_version 10.2.2.0) honour the 0 literally (corpus
quadruplet at Δ 2.56, fans account for 1.42).
- **"insulated (assumed)" roof ≠ observed retrofit:** RdSAP renders
"(assumed)" when insulation PRESENCE is an age-band assumption — Table 18
age default governs, for BOTH the "ND" and "NI" thickness sentinels
(which are lodging-software noise). The §5.11.4 50 mm row (U 0.68) is
only for the bare "insulated" description (observed retrofit, depth
unknown). Fixed in u_roof (corpus 74.2% → 75.5%); unit-pinned in
test_rdsap_uvalues.
- **MAPPER GAP — cylinder insulation thickness dropped (RdSAP-17.0+):** the mapper
carries `cylinder_size` + `cylinder_insulation_type` but NOT
`cylinder_insulation_thickness` → `EpcPropertyData.sap_heating.cylinder_

View file

@ -0,0 +1,281 @@
---
name: find-weird-recommendations
description: Hunt for dubious *recommendations* in a modelled portfolio — measures that were wrongly withheld (SAP could have moved to the goal band but didn't), measures that contradict the dwelling (e.g. HHRSH on community heating), and neighbours of the same type/postcode that were modelled differently. Deterministic scan → neighbour scan → deep-dive with the live re-model → root-cause → codify. Use when the tech team flags "weird" or "dubious" recommendations on a portfolio and wants to understand why the engine did it. Asks for portfolio id and scenario id.
---
# Find weird recommendations
Sibling to `audit-ara-portfolio`. That skill audits **baselines, plans and SAP
scores**; this one audits the **recommendations themselves** — *why a measure was
or wasn't offered*. It exists because the tech team keeps finding individual
dodgy recommendations (portfolio 814: Khalim's HHRSH-on-community-heating,
missing-HHRSH, missing-secondary-heating-removal, and a neighbour split) and we
want a repeatable way to turn "this one looks wrong" into a root cause and a
durable check that makes the engine better.
The engine's job on an unlimited-budget scenario is to move each dwelling to the
goal band (usually C) with a sensible, physically-valid set of measures.
"Weird" = the engine did something a competent surveyor wouldn't: **left SAP on
the table**, **recommended a measure the dwelling can't take**, or **treated two
identical neighbours differently**. Each has a deterministic tell; this skill
runs the tells, then uses the live re-model to explain each hit.
## Input
Ask for **portfolio_id** and **scenario_id** if the user didn't give them. The
scenario matters here: its `goal_value` is the band we expect the plan to reach,
and its `budget` decides whether a shortfall is a bug or just "the money ran
out". Without a scenario_id the deterministic scan audits each Property's
*default* plan (the FE-shown one).
## Query safety (READ FIRST — inherited from `audit-ara-portfolio`)
The `recommendation` table is **~26m rows with NO index on `plan_id`**. Any query
reaching it via `plan_id` (a `JOIN ... ON r.plan_id = pl.id`, or a correlated
`EXISTS`) forces a full seq-scan and can take the shared DB down — this is what
blocked the portfolio-796 audit. The full rules are in the `audit-ara-portfolio`
skill; the ones that bite *here*:
- **Confirm any ad-hoc `recommendation` SQL with the user, and show the
`EXPLAIN` plan first** (no `ANALYZE`). If it contains `Seq Scan on
recommendation`, do NOT run it.
- **Reach `recommendation` only via the indexed `property_id`, scoped to a
handful of flagged property ids** — never portfolio-wide, never via `plan_id`.
- The two scripts below are safe by construction: `anomalies.py`'s recommendation
rollup is opt-in + EXPLAIN-gated, and `neighbour_divergence.py` never touches
`recommendation` at all. The measure-set comparison that Khalim's neighbour
case needs is done in the **deep-dive** (Phase 4) with the live re-model, not a
portfolio-wide `recommendation` query.
## Phase 1 — Deterministic recommendation scan
```
python -m scripts.audit.anomalies --portfolio <portfolio_id> --scenario <scenario_id>
```
Writes `modelling_audit.md` / `.csv`. The check that carries this skill:
- **`plan-stops-short-of-goal`** (HIGH) — the default plan ends *below* the goal
band on an **unlimited-budget** scenario. This is the deterministic worklist
for the "why didn't this get recommended X" class (Khalim's pid 742121). With
no budget cap, the only reasons to fall short are (a) a measure was wrongly
withheld — a bug — or (b) the dwelling genuinely can't reach the band. Phase 4
decides which. Budget-capped scenarios are excluded (falling short is expected).
**Caveat: this check is noisy on flat-heavy portfolios** — it fired 322/338 on
portfolio 814 (every flat a band short of B). Use `plan-stuck-in-low-band` to
cut straight to the real hits.
- **`plan-stuck-in-low-band`** (HIGH) — the plan leaves the dwelling **still
rated E/F/G with the band unchanged** from baseline (unlimited budget). This is
the sharp "E→E, no big movement" worklist — the genuinely-trapped tail, not
every dwelling a band short of goal. On portfolio 814 it flagged just **4**
(pids 742121, 742210, 742265, 742347) vs `plan-stops-short-of-goal`'s 322. A
wrongly-withheld measure or a physically-trapped dwelling both surface here;
Phase 4 tells them apart. Band-keyed (no SAP-points threshold), and the
strictly-worse case is left to `plan-below-baseline-band` so the two partition.
Also read `plan-below-baseline-band`, `already-meets-goal-with-works`, and — if
you pass `--with-recommendations` (EXPLAIN-gated; confirm with the user first) —
`excessive-solar-sap` / `low-solar-bill-savings`. These bound the "the plan did
something odd" surface.
## Phase 2 — Neighbour-divergence scan
```
python -m scripts.audit.neighbour_divergence --portfolio <portfolio_id>
```
Writes `neighbour_divergence.md` / `.csv`. Groups the portfolio into cohorts of
(postcode, property_type, built_form) and flags any member whose **effective
baseline SAP** sits `--min-gap` (default 12) points from its cohort median.
Near-identical neighbours should model alike; a SAP split is what drives a
recommendation split (Khalim's neighbour case). This is the SAP half of the
heuristic — cheap and portfolio-wide because it never touches `recommendation`.
The **measure-set** half is Phase 4: re-model both neighbours and diff their
plans.
Floor area is reported, not keyed on — discount a genuinely bigger/smaller
neighbour before trusting a hit.
## Phase 3 — Triage the hits
For each group, HIGH first: note the count, read a few rows, and give a one-line
**root-cause hypothesis** + a verdict (real bug / expected / threshold to tune).
Cross-check the two scans against each other — a `plan-stops-short-of-goal`
property that is *also* a divergent neighbour is a high-value case (the two
signals agree).
## Phase 4 — Deep-dive with the live re-model (the core of this skill)
Reproduce end-to-end, NO DB writes (never pass `--persist`):
```
python -m scripts.run_modelling_e2e <property_id> [<neighbour_id> ...] --scenario-id <scenario_id>
```
This re-fetches the live EPC + solar and runs the full DDD Modelling stage. It
writes three files — read all three:
- `modelling_e2e.md` — the chosen plan, measure by measure, with post-SAP.
- `modelling_e2e_candidates.csv` — **every candidate Option the generators
produced, including the ones the Optimiser didn't pick.** This is how you tell
a *withheld* measure from a *rejected* one:
- Measure **absent from candidates** → a **generator eligibility gate**
excluded it. This is the bug surface for "why no HHRSH / no
secondary_heating_removal". Open the generator and read its `_*_eligible`
predicate against the live EPC.
- Measure **present in candidates but not chosen** → the Optimiser judged it
not worth it (cost/SAP/goal already met). Usually correct; confirm the goal
was reached without it.
- `modelling_e2e.csv` — the row-level plan.
For a neighbour pair, run **both** in one invocation and diff their candidate
sets and chosen plans — that is the measure-set comparison Khalim asked for,
done safely (no `recommendation` table).
### Known recommendation-bug patterns (seed catalogue — grew from portfolio 814)
Match each deep-dive against these before hypothesising a novel cause:
- **HHRSH offered on a community-heated dwelling** (pids 742174, 742175).
**Confirmed bug.** The DDD generator's `_hhr_storage_eligible`
([`domain/modelling/generators/heating_recommendation.py`](../../../domain/modelling/generators/heating_recommendation.py))
returns eligible when the dwelling is `off_gas` — but a community-heated
dwelling is off-gas, and the legacy engine's
`is_high_heat_retention_valid` ([`recommendations/HeatingRecommender.py`](../../../recommendations/HeatingRecommender.py))
gated on `main_fuel["is_community"]` and refused it. The community-heating
guard was **lost in the legacy→DDD translation**. Confirm by checking the EPC's
main fuel / heat-network flag, then fix by restoring the gate in
`_hhr_storage_eligible` (community heating ⇒ not HHR-eligible). A community
network is a shared asset a single dwelling can't rip out for storage heaters.
- **Measure withheld → plan stops short of goal** (the "why no HHRSH" class).
The dwelling can't reach the goal band, and the expected bundle is *absent from
candidates*. Read the generator's eligibility predicate against the live EPC —
an over-tight gate (a fuel/description/category condition that shouldn't
exclude this dwelling) is the usual cause.
- **Measure OFFERED but not selected because it *lowers* SAP** (pid 742121, "why
no HHRSH" — re-modelled 2026-07, NOT the stale mains-gas story). This dwelling
is an **electric** maisonette (`main_fuel_type=29`), so HHRSH **is** in the
candidate set — eligibility passes. But scoring the HHRSH overlay in isolation
gives **6.3 SAP** (54.3 → 48.0): swapping its efficient existing electric
system for off-peak storage heaters *worsens* the cost-based SAP, so the
Optimiser correctly drops it. Its fabric is already done (double-glazed, walls
insulated, no cylinder, no own roof), so the plan is **empty** and it sits at E,
unreachable to B — a legitimate £0 result, not a bug. **Lesson: a measure
*absent from candidates* is a generator-gate bug; a measure *present but not
chosen* is the Optimiser judging it not worth it — score the overlay
(`PackageScorer.score(epc, [EpcSimulation(...)])`) before blaming a gate.** The
old note here claimed 742121 was a mains-gas maisonette that "correctly gets
only lighting" — that was stale; verify fuel against the live EPC every time.
- **Community-heated flat stuck low, correctly** (pid 742265, "might need loft
insulation?"). A **community-heated** flat (SAP main-heating code 301, category
6) on portfolio 814. HHRSH and ASHP are both gated out by `is_heat_network_main`
(the topology guard) — right, per the community bug above. Its roof is
`"Pitched (vaulted ceiling)"` with insulation lodged as **`ND` (no data)**, so
the loft generator withholds insulation: it only fires on a definite `0 mm`
(`roof_insulation_thickness != _ROOF_UNINSULATED_MM` → None), and a vaulted
ceiling isn't the loft-fallback's target anyway. Its only real lever is IWI
(+6.7), not enough to clear E — so it stays E→E, legitimately. **Watch item:**
a vaulted-ceiling roof with `ND` insulation gets *no* roof measure at all
(neither the sloping-ceiling branch, which matches the literal `"sloping
ceiling"`, nor the loft branch, which needs `0 mm`). If several such roofs turn
out to be genuinely uninsulated upstream, that's a mapper/gate gap — but `ND`
is unknown, not zero, so withholding is defensible for now.
- **Missing `secondary_heating_removal`** (pid 742264, "gov site shows secondary
heating"). Check whether the live EPC carries a secondary-heating system and
whether
[`domain/modelling/generators/secondary_heating_recommendation.py`](../../../domain/modelling/generators/secondary_heating_recommendation.py)
detects it. A gap between the gov register's secondary-heating field and what
our EpcPropertyData mapper carries is a common upstream cause — verify the
mapped value, not just the register.
- **Neighbour split** — two same-cohort dwellings, one gets a bundle the other
doesn't. Almost always traces to a **baseline-input divergence** (a different
mapped fuel / heating code / floor area / an override on one and not the
other), which then flips a generator's eligibility gate. Diff the two EPCs'
mapped inputs first; the recommendation split is downstream of that.
## Phase 5 — Root-cause and cross-reference open work
Isolate **where** it goes wrong: a generator eligibility gate, the EPC→
EpcPropertyData mapping, an override, or the Optimiser. Then check nothing is
already in flight:
```
gh pr list --repo Hestia-Homes/Model --state open
gh pr list --repo Hestia-Homes/Model --state all --search "<keyword>"
```
Map each confirmed cause to an existing PR/ADR or flag it as new work. State,
per case, the smallest correct fix (usually: tighten/loosen one generator
predicate, or fix one mapper field) and which real property ids reproduce it.
## Phase 6 — Self-improve (the compounding loop)
When the review confirms a **novel, systematic** recommendation problem, codify
it so every future run catches it — same gates as `audit-ara-portfolio` Phase 6:
1. **Systematic** — reproduced on **≥ 5** properties and root-caused, not a
one-off (a single weird property is a ticket, not a check).
2. **Not already covered** — no existing check fires on it; no open/merged PR or
ADR already addresses the cause.
3. **Pressure-tested** — for any threshold/heuristic, run `/grill-me` on the
proposed check: false-positive rate on this portfolio? threshold defensible
against the real distribution? overlaps an existing check?
**What to change, smallest first:**
- **A per-property tell** → a decorated `(PropertyAudit) -> Optional[str]` check
in `scripts/audit/anomalies.py` (docstring records the motivating pids + the
one-line cause, like `plan-stops-short-of-goal` does). Extend the
`PropertyAudit` bundle + `_QUERY` if it needs a field — keep every query bounded
by the portfolio and off the `recommendation` table's `plan_id`.
- **A cross-property tell** (a neighbour/cohort pattern) → a detector in
`scripts/audit/neighbour_divergence.py`. The obvious next one: promote the
measure-set diff into a scripted check that reaches `recommendation` via
property-id-scoped, EXPLAIN-gated queries per cohort (kept out of v1 to stay
portfolio-wide-safe — do it once the SAP tell's false-positive rate is known).
- **This catalogue** → add any newly-confirmed bug pattern to Phase 4's list with
its pids and root cause, so the next reviewer starts ahead.
Commit each codified check on its own referencing the motivating run, then re-run
Phases 12 to confirm it fires on the motivating cases and nothing surprising
else. The check registry — with provenance — is the durable output of every hunt.
## Notes
- Read-only on the DB. `run_modelling_e2e` is a dry run — never `--persist`.
- **Two engines exist.** Portfolios are modelled by the **DDD** engine
(`domain/modelling/generators/…`, what `run_modelling_e2e` runs); the legacy
`recommendations/…` engine is the historical reference. When a gate looks
wrong, compare the DDD predicate against its legacy counterpart — several
portfolio-814 bugs are gates the DDD translation **dropped** (community heating
is the confirmed one). The legacy code is the spec of intent, not dead code.
- **Stored plan can be STALE vs live.** The persisted default plan is an earlier
run's output; `run_modelling_e2e` re-models against current logic + live EPC.
A stored-vs-live gap is a big driver of Phase 1 hits and is fixed by
re-modelling, not by debugging the calculator — confirm a sample with the
re-model before blaming a generator (see `audit-ara-portfolio` Notes).
- A `plan-stops-short-of-goal` hit is **not automatically a bug** — the user's
own framing: "surely there's a reason, but if a sensible reason we should
understand why". The deliverable for a physically-unreachable dwelling is the
*explanation* (which measures were tried, why the band is out of reach), not a
code change.
- **On flat/maisonette-heavy portfolios, `plan-stops-short-of-goal` fires in
bulk — mostly by design, not bugs.** Characterise the cohort by property_type
and post-band before deep-diving (cheap: `property` + `epc_property` + `plan`,
no `recommendation` table). Portfolio 814 (94% flats/maisonettes, goal band B)
landed 311/338 short of B, but **every** D-or-worse lander and every £0-works
shortfall was a flat/maisonette — all 19 houses reached C+. The structural
cause: the heating-decarbonisation levers are gated away from flats —
`_ashp_eligible` offers ASHP **only to houses/bungalows** (`_is_house_or_bungalow`),
the gas-boiler upgrade needs an existing **wet** boiler (point-of-use gas water
heaters like WHC 908 don't qualify — see pid 742121, a mains-gas maisonette at
E that correctly gets only lighting), and HHRSH needs electric/off-gas. So a
mains-gas or community-heated flat with insulated walls + DG has almost no lever
and lands at D/E legitimately. Two takeaways: (1) triage the *houses* short of
goal first — those are the real bugs; (2) the flat cohort is a **product**
question (should ASHP/communal-heat measures be offered to flats?), not a
per-property modelling bug — surface it as such.
- **A measure can be SELECTED while *lowering* SAP** if it's a forced companion
(pid 742175: `mechanical_ventilation` at **4.1 SAP**, bundled after
`cavity_wall_insulation`). Net plan SAP still rose, but the companion drags it
and worsens the bill — worth a look when a plan's post-SAP barely moves or dips
(overlaps `plan-score-below-baseline`). Not yet root-caused; note the pids.

View file

@ -101,6 +101,18 @@ _Avoid_: energy assessment, site survey, field survey, Domna survey, Hestia surv
Property data supplied by a landlord that may correct or supplement the public EPC for a single Property; triggers Rebaselining when applied; not applicable when Site Notes are present.
_Avoid_: patches (deprecated), corrections, manual EPC, edits
**Landlord-Description Classification**:
Resolving a **Landlord Description** (unbounded free-text a landlord supplies for one component — "CWI" / "Cav filled" / "cavity insulated" all name one thing) onto a **Recognised Internal Description** via an LLM classifier, persisted in the `landlord_*_overrides` table (`source=classifier`) as a reviewed cache. Four vocabularies are kept **distinct** and must not be conflated: a **Landlord Description** (unbounded input); a **Recognised Internal Description** (the closed target taxonomy — e.g. a `MainHeatingSystemType` archetype — each binding to a Simulation Overlay); a **Lodged Description** (the gov-EPC `main_heating[].description` rendering, e.g. "Room heaters, electric" — only an example of which system *types* occur, never a map key); and the **SAP main heating code** (Table 4a/4b, what the calculator consumes). The classifier maps Landlord → Recognised Internal → SAP code. When it cannot confidently place the text it emits **`None`** (no overlay → the lodged EPC stands, surfaced to the user as "no suitable match"), **never the nearest wrong archetype** — the target taxonomy must be complete enough that a real system always has a correct home, so the classifier never overflows into a garbage-drawer archetype (ADR-0041).
**Heating Companion Set**:
The coherent SAP inputs a **main heating system** override drags alongside the **SAP main heating code** so the **Effective EPC** reads as one internally-consistent system, not a hybrid of the new code and the *replaced* system's leftovers — heating **category** (SAP Table 4a group: gas→2, heat pump→4, network→6, storage→7, electric underfloor→8, warm air→9, room heater→10), **charge/heating control** (Table 4e group), **natural fuel**, and **meter** (see **Off-Peak Meter**). Companions divide by how firmly the archetype fixes them (ADR-0048): **category and control are archetype-forced** — always written, never inherited, because a wrong value silently mis-scores or mis-bills (a leftover storage **category 7** makes the Table 12a resolver bill peaky room heaters at the all-night rate; a leftover storage **control 2401** adds a +0.7 °C Table-4e penalty a room heater should not carry). **Fuel and meter defer** only where multiple values are genuinely coherent — fuel for an ambiguous carrier (community heating: gas CHP / biomass / waste heat), meter for a meter-flexible system (room heaters run on either; screed underfloor per ADR-0046) — while a meter-*locked* system still forces (storage/CPSU/HHRSH→Dual, non-electric→Single, ADR-0035). Because the overlay composes last-wins, an **unset (`None`)** forced companion is *inherited from the replaced system*, not cleared — so an archetype the overlay cannot yet fully companion (heat pump / community / underfloor / electric boiler control defaults are unmapped) is **logged as an error and continues** (log-not-raise, matching `flag_fuel_mismatch`), making the gap visible for review rather than shipping a silently-incoherent cert; the intended end-state is to fill each and flip the log to a raise.
_Avoid_: coherent companions (informal), dragged fields, heating defaults
_Avoid_: "the LLM mapper is unreliable" (the failure mode is a too-small target taxonomy, not LLM language ability); conflating the landlord input vocabulary with the gov-EPC lodged rendering or the RdSAP entry-tool catalogue; treating a deterministic dict as a *replacement* for the LLM rather than a reviewed cache of its output
**Lodgement Sentinels (ND / NI / AB)**:
The RdSAP string sentinels a cert may lodge in place of a numeric value (e.g. `roof_insulation_thickness`). They are data, not noise, and are **not interchangeable**: **`ND`** (Not Defined) — no data; resolve to the construction age band's **as-built** state (roof as-built: AD uninsulated, EF limited insulation, G+ insulated). **`NI`** — insulation **present** but thickness unknown (RdSAP 10 §5.11.4: score as 50 mm); never "no insulation". **`AB`** — As Built; resolve by age band like ND. The mapper deliberately passes roof sentinels through raw — the calculator's resolution depends on distinguishing them (ADR-0047). Scoring and eligibility may legitimately resolve the same sentinel differently (`NI`: 50 mm to the U-cascade, not-eligible to the roof generator).
_Avoid_: reading `ND` as "no insulation"; reading `NI` as "no insulation" (it means the opposite); normalising sentinels at the mapper boundary; comparing a sentinel string against an int (`"ND" != 0` is always True)
### Modelling
**Effective EPC**:
@ -116,11 +128,11 @@ Deterministically translating an **old / reduced-data EPC schema** into the curr
_Avoid_: gap-fill (means the neighbour-ML path), reduced-data expansion (overloaded with the calculator's Table-5 step), remapping (the schema-translation part only)
**Baseline Performance**:
A Property's current performance aggregate, holding both Lodged Performance and Effective Performance plus the energy block: delivered kWh **per end use** (heating, hot water, lighting, appliances, cooking, pumps/fans, cooling) and the **annual bill** composed into per-section costs plus a total, produced by **Bill Derivation** from SAP10 Calculation's per-end-use kWh × current Fuel Rates. Persisted as one row (flat typed columns, per-section kWh + cost + total); surfaced as one block in the UI.
A Property's current performance aggregate, holding both Lodged Performance and Effective Performance plus the energy block: delivered kWh **per end use** (heating, hot water, lighting, appliances, cooking, pumps/fans, cooling) and the **annual bill** composed into per-section costs plus a total, produced by **Bill Derivation** from SAP10 Calculation's per-end-use kWh × current Fuel Rates. Persisted as one row (flat typed columns, per-section kWh + cost + total); surfaced as one block in the UI. The **Lodged half is optional**: a predicted Property has no lodged record (see Lodged Performance), so its `lodged_*` are `NULL` and only the Effective half + energy block are populated (ADR-0004 amendment, #1361).
_Avoid_: baseline predictions, predicted baseline, rebaselined values
**Lodged Performance**:
The SAP / EPC Band / carbon emissions / Primary Energy Intensity recorded on the public EPC (or the Site Notes' as-surveyed values when Site Notes are the source) — unmodified by modelling. The half of Baseline Performance that says "what the government register says about this Property".
The SAP / EPC Band / carbon emissions / Primary Energy Intensity recorded on the public EPC (or the Site Notes' as-surveyed values when Site Notes are the source) — unmodified by modelling. The half of Baseline Performance that says "what the government register says about this Property". **Requires a record _of this Property_** — a lodged cert or a Site Notes survey — so it is **absent (`None`/NULL) for a predicted Property**: **EPC Prediction** synthesises the picture from neighbours, copying a comparable's recorded figures, so there is no government-register record of _this_ Property to lodge. Only the Effective half is persisted then (ADR-0004 amendment, #1361). Reading a predicted EPC's recorded fields as Lodged Performance manufactures a phantom — a neighbour's SAP presented as this Property's lodged figure.
_Avoid_: original performance, raw EPC values, recorded baseline
**Effective Performance**:
@ -305,7 +317,11 @@ The two competing **Measure Options** for insulating a *solid* (non-cavity) main
_Avoid_: solid wall insulation (that names the pair, not one Option), cladding, drylining
**Wall Insulation Eligibility**:
The rule fixing which wall Option(s) the main-wall **Recommendation** offers, by wall construction then planning status. By construction: **cavity** → cavity fill only (never solid insulation); **solid brick** / **system-built** → IWI + EWI; **timber-frame** → IWI only (EWI is not constructable); **cob** / **granite or whinstone** / **sandstone or limestone** → none (breathable fabric — standard insulation risks trapping moisture). Then, as planning gates: a **conservation area** or **flat** removes EWI (external-appearance / whole-block constraints), and a **listed** or **heritage** building removes **both** EWI and IWI (protected fabric).
The rule fixing which wall Option(s) the main-wall **Recommendation** offers, by wall construction then planning status. By construction: **cavity** → cavity fill only (never solid insulation); **solid brick** / **system-built** → IWI + EWI; **timber-frame** → IWI only (EWI is not constructable); **cob** / **granite or whinstone** / **sandstone or limestone** → none (breathable fabric — standard insulation risks trapping moisture). Then, as planning gates: a **conservation area** or **flat** removes EWI (external-appearance / whole-block constraints), and a **listed** or **heritage** building removes **both** EWI and IWI (protected fabric). Finally the **Wall U-Value Gate** applies.
**Wall U-Value Gate**:
The eligibility rule that a wall insulation Option is offered **only when it would lower the wall's derived U-value** (by more than a 0.01 W/m²K float-noise guard) — comparing `u_wall` on the effective wall's current state vs its post-measure state (ADR-0051). Kills the **no-op fabric offer**: on a modern construction age band (e.g. band L, 20122022) RdSAP's cascade assigns the as-built wall its at-regs U-value and added insulation resolves to the *same* value, so the measure costs thousands for +0.00 SAP. The gate is physical possibility only — how *much* improvement is worth the cost stays with the Optimiser (ADR-0016/0024).
_Avoid_: no-op filter (it gates generation, not scored output), U-value threshold (there is no absolute target-U; the comparison is before-vs-after)
_Avoid_: restricted measures (legacy collapsed conservation/listed/heritage into one boolean — they now gate different Options, so keep them distinct)
**Roof Insulation Eligibility**:

View file

@ -0,0 +1,11 @@
version: "3.9"
services:
audit-generator-lambda:
build:
context: ../../../
dockerfile: applications/audit_generator/handler/Dockerfile
ports:
- "9001:8080"
env_file:
- ../../../.env

View file

@ -0,0 +1,29 @@
#!/usr/bin/env python3
import json
import requests
HOST = "localhost"
PORT = "9001"
LAMBDA_URL = f"http://{HOST}:{PORT}/2015-03-31/functions/function/invocations"
payload = {
"Records": [
{
"messageId": "test-message-id",
"body": json.dumps(
{
"task_id": "00000000-0000-0000-0000-000000000001",
"sub_task_id": "00000000-0000-0000-0000-000000000002",
"hubspot_deal_id": "500311510234",
}
),
}
]
}
response = requests.post(LAMBDA_URL, json=payload)
print("Status code:", response.status_code)
print("Response:")
print(response.text)

View file

@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Run AuditGeneratorOrchestrator directly, bypassing @subtask_handler.
Loads credentials from the repo-root .env file so no DB task/subtask rows
are needed.
"""
import os
import sys
from pathlib import Path
from typing import Any
import boto3
from dotenv import load_dotenv
REPO_ROOT = Path(__file__).resolve().parents[3]
load_dotenv(REPO_ROOT / ".env")
sys.path.insert(0, str(REPO_ROOT))
from utilities.logger import setup_logger
setup_logger()
from infrastructure.postgres.config import PostgresConfig
from infrastructure.postgres.engine import make_engine, make_session
from infrastructure.s3.s3_client import S3Client
from orchestration.audit_generator_orchestrator import AuditGeneratorOrchestrator
from orchestration.audit_generator_unit_of_work import AuditGeneratorUnitOfWork
HUBSPOT_DEAL_ID = "505712829658"
boto3_client: Any = boto3.client # type: ignore[attr-defined]
s3_client = S3Client(
boto_s3_client=boto3_client("s3"),
# bucket=os.environ["S3_BUCKET_NAME"],
bucket="retrofit-energy-assessments-dev",
)
engine = make_engine(PostgresConfig.from_env(os.environ))
def session_factory() -> Any:
return make_session(engine)
def uow_factory() -> AuditGeneratorUnitOfWork:
return AuditGeneratorUnitOfWork(session_factory)
print(f"Running AuditGeneratorOrchestrator for deal: {HUBSPOT_DEAL_ID!r}")
AuditGeneratorOrchestrator(
hubspot_deal_id=HUBSPOT_DEAL_ID,
s3_client=s3_client,
uow_factory=uow_factory,
).run()
print("Done")

View file

@ -10,11 +10,22 @@ from applications.landlord_description_overrides.landlord_description_overrides_
from domain.epc.property_overrides.built_form_type import BuiltFormType
from domain.epc.property_overrides.construction_age_band import ConstructionAgeBand
from domain.epc.property_overrides.glazing_type import GlazingType
from domain.epc.property_overrides.glazing_mix_guard import glazing_mix_guard
from domain.epc.property_overrides.main_fuel_type import MainFuelType
from domain.epc.property_overrides.main_fuel_guard import main_fuel_guard
from domain.epc.property_overrides.main_heating_system_type import MainHeatingSystemType
from domain.epc.property_overrides.main_heating_guard import main_heating_guard
from domain.epc.property_overrides.property_type import PropertyType
from domain.epc.property_overrides.property_type_guard import property_type_guard
from domain.epc.property_overrides.roof_type import RoofType
from domain.epc.property_overrides.roof_party_ceiling_guard import (
roof_party_ceiling_guard,
)
from domain.data_transformation.guarded_column_classifier import (
GuardedColumnClassifier,
)
from domain.epc.property_overrides.water_heating_type import WaterHeatingType
from domain.epc.property_overrides.water_heating_guard import water_heating_guard
from domain.epc.property_overrides.wall_type import WallType
from domain.epc.property_overrides.wall_type_construction_dates import (
wall_type_construction_date_prompt_hint,
@ -82,8 +93,15 @@ def _build_columns(
"property_type": lambda src: ClassifiableColumn(
name="property_type",
source_column=src,
classifier=ChatGptColumnClassifier(
chat_gpt, PropertyType, PropertyType.UNKNOWN
# The dwelling type is the leading token of the "<type>: <built form>"
# split; the deterministic guard claims it so the built-form tail can't
# flip the type (the LLM read "Bungalow: EndTerrace" as House), and the
# LLM handles varied phrasings (#1376).
classifier=GuardedColumnClassifier(
guard=property_type_guard,
fallback=ChatGptColumnClassifier(
chat_gpt, PropertyType, PropertyType.UNKNOWN
),
),
repo=LandlordOverridesRepository[PropertyType](
session, LandlordPropertyTypeOverrideRow
@ -115,8 +133,13 @@ def _build_columns(
"roof_type": lambda src: ClassifiableColumn(
name="roof_type",
source_column=src,
classifier=ChatGptColumnClassifier(
chat_gpt, RoofType, RoofType.UNKNOWN
# A party ceiling ("another/same dwelling or premises above") has ~0
# heat loss and must never be classified as an external roof; the
# deterministic guard resolves those markers and the LLM handles the
# rest (#1376).
classifier=GuardedColumnClassifier(
guard=roof_party_ceiling_guard,
fallback=ChatGptColumnClassifier(chat_gpt, RoofType, RoofType.UNKNOWN),
),
repo=LandlordOverridesRepository[RoofType](
session, LandlordRoofTypeOverrideRow
@ -125,8 +148,14 @@ def _build_columns(
"main_fuel": lambda src: ClassifiableColumn(
name="main_fuel",
source_column=src,
classifier=ChatGptColumnClassifier(
chat_gpt, MainFuelType, MainFuelType.UNKNOWN
# An individual wood-logs fuel had no LLM target and was funnelled into
# "biomass (community)"; the deterministic guard resolves it and the LLM
# handles the rest (#1376).
classifier=GuardedColumnClassifier(
guard=main_fuel_guard,
fallback=ChatGptColumnClassifier(
chat_gpt, MainFuelType, MainFuelType.UNKNOWN
),
),
repo=LandlordOverridesRepository[MainFuelType](
session, LandlordMainFuelOverrideRow
@ -135,8 +164,15 @@ def _build_columns(
"glazing": lambda src: ClassifiableColumn(
name="glazing",
source_column=src,
classifier=ChatGptColumnClassifier(
chat_gpt, GlazingType, GlazingType.UNKNOWN
# An aggregate glazing mix ("40% double, 60% single") can't be applied
# per-window, so the deterministic guard resolves the structured split
# to MIXED (no overlay → keep the cert's per-window glazing) and the LLM
# handles uniform / varied phrasings (#1376, ADR-0042).
classifier=GuardedColumnClassifier(
guard=glazing_mix_guard,
fallback=ChatGptColumnClassifier(
chat_gpt, GlazingType, GlazingType.UNKNOWN
),
),
repo=LandlordOverridesRepository[GlazingType](
session, LandlordGlazingOverrideRow
@ -155,8 +191,16 @@ def _build_columns(
"water_heating": lambda src: ClassifiableColumn(
name="water_heating",
source_column=src,
classifier=ChatGptColumnClassifier(
chat_gpt, WaterHeatingType, WaterHeatingType.UNKNOWN
# A biomass / wood / dual-fuel / biodiesel DHW description has no
# dedicated LLM target and was funnelled into "house coal"; the
# deterministic guard resolves the structured fuels (and the "electric
# immersion assumed" no-system case) and the LLM handles the rest
# (#1376, ADR-0043).
classifier=GuardedColumnClassifier(
guard=water_heating_guard,
fallback=ChatGptColumnClassifier(
chat_gpt, WaterHeatingType, WaterHeatingType.UNKNOWN
),
),
repo=LandlordOverridesRepository[WaterHeatingType](
session, LandlordWaterHeatingOverrideRow
@ -165,8 +209,15 @@ def _build_columns(
"main_heating_system": lambda src: ClassifiableColumn(
name="main_heating_system",
source_column=src,
classifier=ChatGptColumnClassifier(
chat_gpt, MainHeatingSystemType, MainHeatingSystemType.UNKNOWN
# High heat retention storage heaters have no LLM target distinct from
# old storage, so they were funnelled into "Electric storage heaters,
# old" (401); the deterministic guard resolves HHRSH to its own
# archetype (SAP 409) and the LLM handles the rest (#1376, ADR-0044).
classifier=GuardedColumnClassifier(
guard=main_heating_guard,
fallback=ChatGptColumnClassifier(
chat_gpt, MainHeatingSystemType, MainHeatingSystemType.UNKNOWN
),
),
repo=LandlordOverridesRepository[MainHeatingSystemType](
session, LandlordMainHeatingSystemOverrideRow

View file

@ -108,7 +108,10 @@ from repositories.product.composite_product_repository import (
from repositories.property.in_memory_property_overrides_reader import (
InMemoryPropertyOverridesReader,
)
from repositories.property.landlord_override_overlays import overlays_from
from repositories.property.landlord_override_overlays import (
flag_fuel_mismatch,
overlays_from,
)
from repositories.property.override_backed_prediction_attributes_reader import (
OverrideBackedPredictionAttributesReader,
)
@ -509,12 +512,11 @@ def handler(
# held-open read Session. (The ``finally`` is the safety net.)
scenario = ScenarioPostgresRepository(read_session).get_many([scenario_id])[0]
products = catalogue_snapshot_with_off_catalogue_overrides(read_session)
# Stored Solar is read whatever refetch_solar says — the flag decides
# whether a UPRN with no stored row gets a live Google fetch, never
# whether solar is modelled at all.
stored_solar: dict[int, Optional[dict[str, Any]]] = (
{}
if not refetch_solar
else SolarPostgresRepository(read_session).get_many(
list(set(uprns.values()))
)
SolarPostgresRepository(read_session).get_many(list(set(uprns.values())))
)
epc_repo = EpcPostgresRepository(read_session)
stored_lodged_epcs: dict[int, EpcPropertyData] = (
@ -561,7 +563,9 @@ def handler(
epc = (
None # no stored lodged EPC; prediction path handles this property
)
overrides = overlays_from(overrides_reader.overrides_for(pid))
resolved_overrides = overrides_reader.overrides_for(pid)
flag_fuel_mismatch(resolved_overrides)
overrides = overlays_from(resolved_overrides)
predicted_epc: Optional[EpcPropertyData] = None
predicted_epc_is_new = False
@ -610,15 +614,16 @@ def handler(
landlord_overrides=overrides,
).effective_epc
solar_insights: Optional[dict[str, Any]]
# refetch_solar gates ONLY the paid Google call for UPRNs with no
# stored row; stored insights always feed the Modelling stage.
# (False here once meant "no solar at all" — the pre-rename
# `no_solar` semantics — which silently stripped solar from every
# plan in a re-model batch.)
solar_insights: Optional[dict[str, Any]] = stored_solar.get(uprn)
solar_was_fetched = False
if not refetch_solar:
solar_insights = None
else:
solar_insights = stored_solar.get(uprn)
if solar_insights is None:
solar_insights = _solar_insights_for(solar_client, spatial)
solar_was_fetched = solar_insights is not None
if solar_insights is None and refetch_solar:
solar_insights = _solar_insights_for(solar_client, spatial)
solar_was_fetched = solar_insights is not None
plan = run_modelling(
effective_epc,

View file

@ -8,7 +8,9 @@ from orchestration.sharepoint_renamer_orchestrator import SharepointRenamerOrche
from utils.sharepoint.domna_sharepoint_client import DomnaSharepointClient
from utils.sharepoint.domna_sites import DomnaSites
CSV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sero_address_list.csv")
CSV_PATH = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "sero_address_list.csv"
)
def handler(event: dict[str, Any], context: Any) -> None:

View file

@ -1,182 +1,366 @@
UPRN,Address,Postcode
U1027392,"26 Silverdale Road, Oprington",BR5 2LT
U1003906,"54 Barnsdale Crescent, Oprington",BR5 2AX
U1034479,"90 Mead Way, Bromley",BR2 9EU
U1005549,"79 Lower Gravel Road, Bromley",BR2 8LP
U1016743,"16 Princes Plain, Bromley",BR2 8LE
U1041937,"75 Turpington Lane, Bromley",BR2 8JD
U1034805,"38 Narrow Way, Bromley",BR2 8JB
U1041933,"31 Turpington Lane, Bromley",BR2 8JA
U1037833,"3 Stiles Close, Bromley",BR2 8EQ
U1042734,"86 Whitebeam Avenue, Bromley",BR2 8DW
U1042575,"90 Whitebeam Avenue, Bromley",BR2 8DW
U1033177,"30 Larch Way, Bromley",BR2 8DU
U1027989,"5 Larch Way, Bromley",BR2 8DT
U1012309,"13 Almond Close, Bromley",BR2 8DS
U1022525,"30 Lovelace Avenue, Bromley",BR2 8DQ
U1047613,"13 Whitebeam Avenue, Bromley",BR2 8DJ
U1027549,"14 Thorn Close, Bromley",BR2 8DH
U1022726,"31 Hornbeam Way, Bromley",BR2 8DB
U1021308,"70 Faringdon Avenue, Bromley",BR2 8BU
U1026958,"77 Faringdon Avenue, Bromley",BR2 8BT
U1007553,"115d Faringdon Avenue, Bromley",BR2 8BT
U1032132,"115f Faringdon Avenue, Bromley",BR2 8BT
U1014627,"81 Faringdon Avenue, Bromley",BR2 8BT
U1009607,"20 Parkfield Way, Bromley",BR2 8AF
U1027838,"15 Holmcroft Way, Bromley",BR2 8AD
U1052376,"3 Shoreham Way, Bromley",BR2 7PU
U1015499,"25 Boughton Avenue, Bromley",BR2 7PL
U1005741,"2 Malling Way, Bromley",BR2 7PJ
U1019906,"26 Eastry Avenue, Bromley",BR2 7PF
U1021769,"49 Eastry Avenue, Bromley",BR2 7PE
U1031121,"32 Laburnum Way, Bromley",BR2 8BZ
U1022281,"35 Kingsdown Way, Bromley",BR2 7PT
U1035115,"30 Thorn Close, Bromley",BR2 8DH
U1027493,"35 Sudbury Crescent, Bromley",BR1 4PY
U1016746,"68 Princes Plain, Bromley",BR2 8LE
U1005168,"38 Holbrook Way, Bromley",BR2 8EE
U1036446,"14 Meath Close, Oprington",BR5 2HF
U1010452,"5 Canbury Path, Oprington",BR5 2EU
U1019785,"50 Cray Valley Road, Oprington",BR5 2EZ
U1024065,"64 Marion Crescent, Oprington",BR5 2HD
U1042248,"16 Stanley Way, Oprington",BR5 2HE
U1029229,"2 Meath Close, Oprington",BR5 2HF
U1037768,"13 Silverdale Road, Oprington",BR5 2LU
U1014589,"71 Empress Drive, Chislehurst",BR7 5BQ
U1024698,"4 Palace View, Bromley",BR1 3EL
U1052536,"12 Thorn Close, Bromley",BR2 8DH
U1022018,"12 Hazel Walk, Bromley",BR2 8DF
U1007728,"2 Hazel Walk, Bromley",BR2 8DF
U1002456,"54 Birch Row, Bromley",BR2 8DA
U1020349,"21 Laburnum Way, Bromley",BR2 8BY
U1032129,"78 Faringdon Avenue, Bromley",BR2 8BU
U1032130,"86 Faringdon Avenue, Bromley",BR2 8BU
U1021824,"115g Faringdon Avenue, Bromley",BR2 8BT
U1021827,"121 Faringdon Avenue, Bromley",BR2 8BT
U1026960,"105 Faringdon Avenue, Bromley",BR2 8BT
U1010578,"6 Cranbrook Close, Bromley",BR2 7QA
U1024709,"3 Parkfield Way, Bromley",BR2 8AE
U1024580,"24 Parkfield Way, Bromley",BR2 8AF
U1011190,"11 Shoreham Way, Bromley",BR2 7PU
U1011191,"32 Shoreham Way, Bromley",BR2 7PU
U1021535,"4 Chilham Way, Bromley",BR2 7PR
U1007556,"11 Farleigh Avenue, Bromley",BR2 7PP
U1010255,"7 Boughton Avenue, Bromley",BR2 7PL
U1034810,"24 Newbury Road, Bromley",BR2 0QW
U1009032,"10 Malling Way, Bromley",BR2 7PJ
U1004686,"55 Baston Road, Bromley",BR2 7BD
U1032061,"30 Eastry Avenue, Bromley",BR2 7PF
U1010582,"13 Cranworth Cottages, Keston",BR2 6DB
U1009511,"27 Newbury Road, Bromley",BR2 0QN
U1037954,"28 Treewall Gardens, Bromley",BR1 5BT
U1014793,"59 Headcorn Road, Bromley",BR1 4SQ
U1005123,"24 Bonville Road, Bromley",BR1 4QA
U1037872,"20 Sudbury Crescent, Bromley",BR1 4PZ
U1022305,"18a Lansdowne Road, Bromley",BR1 3LZ
U1035052,"1 Sudbury Crescent, Bromley",BR1 4PY
U1042298,"39 Sudbury Crescent, Bromley",BR1 4PY
U1019564,"32 Brook Lane, Bromley",BR1 4PU
U1024511,"81 Nightingale Lane, Bromley",BR1 2SA
U1032133,"119 Faringdon Avenue, Bromley",BR2 8BT
U1032134,"125 Faringdon Avenue, Bromley",BR2 8BT
U1032131,"93 Faringdon Avenue, Bromley",BR2 8BT
U1021825,"117a Faringdon Avenue, Bromley",BR2 8BT
U1010107,"42 Birch Row, Bromley",BR2 8DA
U1016880,"25 Almond Way, Bromley",BR2 8DR
U1038107,"152 Whitebeam Avenue, Bromley",BR2 8DW
U1052455,"10 Stiles Close, Bromley",BR2 8EQ
U1028328,"76 Magpie Hall Lane, Bromley",BR2 8ER
U1020064,"28 Green Way, Bromley",BR2 8EY
U1041934,"51 Turpington Lane, Bromley",BR2 8JA
U1005696,"158 Magpie Hall Lane, Bromley",BR2 8JG
U1030892,"140 Poverest Road, Oprington",BR5 1RH
U1011072,"45 Rookery Gardens, Oprington",BR5 4BA
U1031058,"303 Keedonwood Road, Bromley",BR1 4QR
U1052429,"76 Southover, Bromley",BR1 4RY
U1042153,"4 Scotts Road, Bromley",BR1 3QD
U1037814,"42 Stanley Road, Bromley",BR2 9JH
U1008158,"71 Lower Gravel Road, Bromley",BR2 8LP
U1032062,"46 Eastry Avenue, Bromley",BR2 7PF
U1016742,"4 Princes Plain, Bromley",BR2 8LE
U1038106,"84 Whitebeam Avenue, Bromley",BR2 8DW
U1013831,"33 Ash Row, Bromley",BR2 8DZ
U1005742,"18 Malling Way, Bromley",BR2 7PJ
U1042572,"37 Whitebeam Avenue, Bromley",BR2 8DJ
U1005167,"30 Holbrook Way, Bromley",BR2 8EE
U1024581,"30 Parkfield Way, Bromley",BR2 8AF
U1024050,"10 Marden Avenue, Bromley",BR2 7PX
U1020328,"31 Kingsdown Way, Bromley",BR2 7PT
U1020327,"5 Kingsdown Way, Bromley",BR2 7PT
U1005131,"10 Boughton Avenue, Bromley",BR2 7PL
U1023415,"27 Malling Way, Bromley",BR2 7PJ
U1052580,"25 Trentham Drive, Oprington",BR5 2EP
U1026447,"50 Princes Plain, Bromley",BR2 8LE
U1007432,"21 Detling Road, Bromley",BR1 4SH
U1053388,"52 Whitebeam Avenue, Bromley",BR2 8DL
U1022307,"20 Larch Way, Bromley",BR2 8DU
U1027743,"17 Whitebeam Avenue, Bromley",BR2 8DJ
U1022533,"81 Lower Gravel Road, Bromley",BR2 8LP
U1014630,"118 Faringdon Avenue, Bromley",BR2 8BU
U1030897,"60 Princes Plain, Bromley",BR2 8LE
U1022931,"15 Lennard Road, Bromley",BR2 8LN
U1042735,"108 Whitebeam Avenue, Bromley",BR2 8DW
U1013830,"31 Ash Row, Bromley",BR2 8DZ
U1020368,"11 Larch Way, Bromley",BR2 8DT
U1020369,"13 Larch Way, Bromley",BR2 8DT
U1042576,"112 Whitebeam Avenue, Bromley",BR2 8DW
U1027830,"26 Holbrook Way, Bromley",BR2 8EE
U1016744,"18 Princes Plain, Bromley",BR2 8LE
U1041772,"13 Stanley Road, Bromley",BR2 9JE
U1042573,"47 Whitebeam Avenue, Bromley",BR2 8DJ
U1008151,"61 Lovelace Avenue, Bromley",BR2 8EA
U1034985,"272 Southborough Lane, Bromley",BR2 8AS
U1007947,"8 Laburnum Way, Bromley",BR2 8BZ
U1027744,"33 Whitebeam Avenue, Bromley",BR2 8DJ
U1038102,"14 Whitebeam Avenue, Bromley",BR2 8DL
U1027745,"148 Whitebeam Avenue, Bromley",BR2 8DW
U1020208,"16 Holbrook Way, Bromley",BR2 8EE
U1023463,"30 MANOR WAY, Bromley",BR2 8ES
U1032647,"32 Narrow Way, Bromley",BR2 8JB
U1033406,"67 Lower Gravel Road, Bromley",BR2 8LP
U1000649,"42 Barnsdale Crescent, Oprington",BR5 2AX
U1036332,"50 Marion Crescent, Oprington",BR5 2HD
U1038103,"29 Whitebeam Avenue, Bromley",BR2 8DJ
U1042574,"66 Whitebeam Avenue, Bromley",BR2 8DL
U1042256,"2 Stiles Close, Bromley",BR2 8EQ
U1034017,"48 Princes Plain, Bromley",BR2 8LE
U1041435,"64 Princes Plain, Bromley",BR2 8LE
U1053386,"27 Whitebeam Avenue, Bromley",BR2 8DJ
U1038104,"32 Whitebeam Avenue, Bromley",BR2 8DL
U1019567,"8 Broom Close, Bromley",BR2 8EU
U1023539,"2 Marsham Close, Chislehurst",BR7 6JD
U1037465,"6 Princes Plain, Bromley",BR2 8LE
U1042733,"68 Whitebeam Avenue, Bromley",BR2 8DL
U1004442,"7 Birch Row, Bromley",BR2 8BX
U1005006,"44 Birch Row, Bromley",BR2 8DA
U1035331,"104 Whitebeam Avenue, Bromley",BR2 8DW
U1024017,"13 Manor Way, Bromley",BR2 8ES
U1014078,"43 Aylesbury Road, Bromley",BR2 0QR
U1052186,"4 Ravensleigh Gardens, Bromley",BR1 5SN
U1037450,"70 Pontefract Road, Bromley",BR1 4RB
U1031294,"70 London Lane, Bromley",BR1 4HE
U1014077,"32 Aylesbury Road, Bromley",BR2 0QP
U1030962,"7 Ravensleigh Gardens, Bromley",BR1 5SN
U1042232,"154 Southover, Bromley",BR1 4RZ
U1024484,"55 Mounthurst Road, Bromley",BR2 7PG
U1007701,"46 Harwood Avenue, Bromley",BR1 3DU
U1020199,"78 Hillside Road, Bromley",BR2 0ST
U1036758,"46 Newbury Road, Bromley",BR2 0QW
U1009369,"17 Minster Road, Bromley",BR1 4DY
U1009194,"84 Mays Hill Road, Bromley",BR2 0HT
U1013358,"3 Bird In Hand Lane, Bromley",BR1 2NA
U1009202,"63 Mead Way, Bromley",BR2 9ER
U1022991,"5 Link Way, Bromley",BR2 8JH
U1025820,"46 Princes Plain, Bromley",BR2 8LE
U1020237,"33 Hornbeam Way, Bromley",BR2 8DB
U1021310,"126 Faringdon Avenue, Bromley",BR2 8BU
U1021353,"66 George Lane, Bromley",BR2 7LQ
U1033165,"2 Laburnum Way, Bromley",BR2 8BZ
U1010811,"13 Gilbert Road, Bromley",BR1 3QP
U1027449,"11 Station Road, Bromley",BR1 3LP
U1035326,"2 Whitebeam Avenue, Bromley",BR2 8DL
U1020351,"27 Laburnum Way, Bromley",BR2 8BY
UPRN,Address,Postcode
U1041316,"U1041316_58 Ravensworth Road, Mottingham",SE9 4LW
U2022716,"U2022716_306 St Helier Avenue, Morden",SM4 6JX
U2018001,"U2018001_37 Leominster Road, Morden",SM4 6HN
U2019131,"U2019131_34 Middleton Road, Morden",SM4 6RT
U1035331,"104 Whitebeam Avenue, Bromley",BR2 8DW
U1010255,"7 Boughton Avenue, Bromley",BR2 7PL
U1024581,"30 Parkfield Way, Bromley",BR2 8AF
U1032130,"86 Faringdon Avenue, Bromley",BR2 8BU
U1014077,"32 Aylesbury Road, Bromley",BR2 0QP
U1007728,"2 Hazel Walk, Bromley",BR2 8DF
U1020327,"5 Kingsdown Way, Bromley",BR2 7PT
U1032134,"125 Faringdon Avenue, Bromley",BR2 8BT
U1053411,"U1053411_78 Windsor Drive, Orpington",BR6 6HF
U2022615,"U2022615_56 St Helier Avenue, Morden",SM4 6LF
U1022991,"5 Link Way, Bromley",BR2 8JH
U1010811,"13 Gilbert Road, Bromley",BR1 3QP
U1020328,"31 Kingsdown Way, Bromley",BR2 7PT
U1005131,"10 Boughton Avenue, Bromley",BR2 7PL
U1021353,"66 George Lane, Bromley",BR2 7LQ
U1034017,"48 Princes Plain, Bromley",BR2 8LE
U1036758,"46 Newbury Road, Bromley",BR2 0QW
U1020351,"27 Laburnum Way, Bromley",BR2 8BY
U1035052,"1 Sudbury Crescent, Bromley",BR1 4PY
U1027073,"U1027073_29 Haddon Road, St Mary Cray",BR5 4BU
U1034887,"U1034887_2 Saltwood Close, Orpington",BR6 9BU
U1035329,"U1035329_79 Whitebeam Avenue, Bromley",BR2 8DN
U1042571,"U1042571_19 Whitebeam Avenue, Bromley",BR2 8DJ
U1035330,"U1035330_87 Whitebeam Avenue, Bromley",BR2 8DN
U1047521,"U1047521_19 Walnuts Road, Orpington",BR6 0RG
U1033037,"U1033037_73 Homefield Rise, Orpington",BR6 0RL
U1027845,"U1027845_92 Homefield Rise, Orpington",BR6 0RW
U1031691,"U1031691_46 Boyland Road, Bromley",BR1 4QF
U1026916,"U1026916_19 Elizabeth Way, St Mary Cray",BR5 4BJ
U1015363,"U1015363_44a Birch Row, Bromley",BR2 8DA
U1010400,"U1010400_42 Brook Lane, Bromley",BR1 4PU
U1052426,"U1052426_49 Southover, Bromley",BR1 4SL
U1019591,"U1019591_18 Buckland Road, Orpington",BR6 9SR
U1011434,"U1011434_24 Upper Elmers End Road, Beckenham",BR3 4AL
U1023417,"U1023417_44 Malling Way, Hayes, Bromley",BR2 7PJ
U1014671,"U1014671_61 George Lane, Hayes, Bromley",BR2 7LG
U1037550,"U1037550_228 Repton Road, Orpington",BR6 9JB
U1014433,"U1014433_57 Cowden Road, Orpington",BR6 0TP
U1015779,"U1015779_21 Beck Lane, Beckenham",BR3 4RD
U1042236,"U1042236_250 Southover, Bromley",BR1 4SE
U1033036,"U1033036_61 Homefield Rise, Orpington",BR6 0RL
U1027951,"U1027951_2 Kingsdown Way, Hayes, Bromley",BR2 7PT
U1032770,"U1032770_18 Pembury Close, Hayes, Bromley",BR2 7PS
U1011129,"U1011129_40 Saltwood Close, Orpington",BR6 9BU
U1023267,"U1023267_47a Mackenzie Road, Beckenham",BR3 4RT
U1016998,"U1016998_68 Arundel Drive, Orpington",BR6 9JG
U1026576,"U1026576_1 Burwash Court, St Mary Cray",BR5 4BG
U1037867,"U1037867_45 Stowe Road, Orpington",BR6 9HG
U1027839,"U1027839_30 Holmcroft Way, Bromley",BR2 8AD
U1005351,"U1005351_2 Leaveland Close, Beckenham",BR3 3PL
U1041968,"U1041968_59 Uplands Road, Orpington",BR6 0RH
U1032149,"U1032149_12 Fordwich Close, Orpington",BR6 0TT
U1047347,"U1047347_30 Sudbury Crescent, Bromley",BR1 4PZ
U1004249,"U1004249_6 Beck Lane, Beckenham",BR3 4RE
U1005772,"U1005772_10 Manor Park Road, West Wickham",BR4 0JZ
U1010810,"U1010810_79 George Lane, Hayes, Bromley",BR2 7LG
U1011008,"U1011008_170 Repton Road, Orpington",BR6 9JA
U1026969,"U1026969_2 Fordwich Close, Orpington",BR6 0TT
U1032000,"U1032000_16 Detling Road, Bromley",BR1 4SH
U1013268,"U1013268_9 Bell Gardens, St Mary Cray",BR5 4DA
U1020217,"U1020217_71 Homefield Rise, Orpington",BR6 0RL
U1033287,"U1033287_12 Lincoln Green Road, St Mary Cray",BR5 2DX
U1041997,"U1041997_31 Walnuts Road, Orpington",BR6 0RG
U1047520,"U1047520_2 Walnuts Road, Orpington",BR6 0RQ
U1019606,"U1019606_20 Burrfield Drive, St Mary Cray",BR5 4BZ
U1007354,"U1007354_30 Cowden Road, Orpington",BR6 0TR
U1015603,"U1015603_1 Barwood Avenue, West Wickham",BR4 0LB
U1035393,"U1035393_18 Yalding Grove, St Mary Cray",BR5 3SE
U1000177,"U1000177_79 Arundel Drive, Orpington",BR6 9JQ
U1011428,"U1011428_21 Uplands Road, Orpington",BR6 0RH
U1024671,"U1024671_115 Oregon Square, Orpington",BR6 8BE
U1021513,"U1021513_18 Chamberlain Crescent, West Wickham",BR4 0LL
U1024663,"U1024663_32b Okemore Gardens, Orpington",BR5 3PJ
U1020879,"U1020879_8 Brockwell Close, St Mary Cray",BR5 1RJ
U1022713,"U1022713_53 Homefield Rise, Orpington",BR6 0RL
U1031157,"U1031157_94 Leesons Hill, St Pauls Cray",BR5 2NH
U1013666,"U1013666_11 Ampleforth Close, Orpington",BR6 9ES
U1052617,"U1052617_44 Uplands Road, Orpington",BR6 0RJ
U1010783,"U1010783_32 Foxbury Drive, Chelsfield, Orpington",BR6 6EL
U1021700,"U1021700_25 Daleside, Orpington",BR6 6EG
U1011163,"U1011163_35 Selwyn Place, St Pauls Cray",BR5 3BA
U1022175,"U1022175_54 Homefield Rise, Orpington",BR6 0RW
U1021461,"U1021461_27 Burrfield Drive, St Mary Cray",BR5 4BZ
U1007184,"U1007184_7 Buckland Road, Orpington",BR6 9SR
U1023112,"U1023112_97 Lower Gravel Road, Bromley",BR2 8LP
U1024516,"U1024516_18 Northolme Rise, Off Tubbenden Lane, Orpington",BR6 9RF
U1023910,"U1023910_40 Moorfield Road, Orpington",BR6 0HQ
U1031342,"U1031342_55 Lower Gravel Road, Bromley",BR2 8LP
U1037905,"U1037905_37 Sweeps Lane, St Mary Cray",BR5 3PE
U2019260,"U2019260_88 Montacute Road, Morden",SM4 6RL
U2019214,"U2019214_11 Missenden Gardens, Morden",SM4 6HW
U2022891,"U2022891_20 Swains Road, Tooting, London",SW17 9HS
U2022684,"U2022684_231 St Helier Avenue, Morden",SM4 6JH
U2023196,"U2023196_210 Tudor Drive, Morden",SM4 4PQ
U2019671,"U2019671_16 Netley Gardens, Morden",SM4 6JW
U2018944,"U2018944_79 Malmesbury Road, Morden",SM4 6HG
U2015873,"U2015873_1 Furness Road, Morden",SM4 6PS
U1041503,"U1041503_103 Ravensworth Road, Mottingham",SE9 4LX
U2023185,"U2023185_157 Tudor Drive, Morden",SM4 4PJ
U1053395,"U1053395_90 Widecombe Road, Mottingham",SE9 4HJ
U2018009,"U2018009_23 Leominster Walk, Morden",SM4 6HB
U2018941,"U2018941_74 Malmesbury Road, Morden",SM4 6HE
U2013950,"U2013950_2 Buckland Walk, Morden",SM4 5RZ
U2018160,"U2018160_6 Llanthony Road, Morden",SM4 6DX
U2015790,"U2015790_12 Flaxley Road, Morden",SM4 6LJ
U2013914,"U2013914_36 Bruton Road, Morden",SM4 5RY
U2022611,"U2022611_50 St Helier Avenue, Morden",SM4 6LF
U2015270,"U2015270_39 Dorchester Road, Morden",SM4 6QE
U1009098,"U1009098_37b Maple Road, Penge",SE20 8LA
U2018926,"U2018926_19 Malmesbury Road, Morden",SM4 6HD
U2023192,"U2023192_195 Tudor Drive, Morden",SM4 4PJ
U1027362,"U1027362_187 Seymour Villas, Penge",SE20 8TP
U2018973,"U2018973_26 Marham Gardens, Morden",SM4 6JJ
U2013913,"U2013913_30 Bruton Road, Morden",SM4 5RY
U1042214,"U1042214_25 Snowdown Close, London",SE20 7RU
U2013911,"U2013911_23 Bruton Road, Morden",SM4 5RY
U2013583,"U2013583_300 Bishopsford Road, Morden",SM4 6BY
U2018925,"U2018925_14 Malmesbury Road, Morden",SM4 6HD
U1037938,"U1037938_5 Torr Road, Penge",SE20 7PS
U1038117,"U1038117_40 Widecombe Road, Mottingham",SE9 4HQ
U1052365,"U1052365_127 Seymour Villas, Penge",SE20 8TP
U1024726,"U1024726_2 Penge Lane, Penge",SE20 7DU
U1026505,"U1026505_7 Bridlington Close, Biggin Hill",TN16 3PD
U2018913,"U2018913_23 Malling Gardens, Morden",SM4 6JG
U2015708,"U2015708_46 Faversham Road, Morden",SM4 6RE
U2019530,"U2019530_30 Neath Gardens, Morden",SM4 6JN
U2015710,"U2015710_49 Faversham Road, Morden",SM4 6RE
U2014478,"U2014478_222 Central Road, Morden",SM4 5PP
U2018922,"U2018922_11 Malmesbury Road, Morden",SM4 6HD
U2013615,"U2013615_47 Blanchland Road, Morden",SM4 5ND
U2022641,"U2022641_127 St Helier Avenue, Morden",SM4 6JD
U2019268,"U2019268_97 Montacute Road, Morden",SM4 6RJ
U2013947,"U2013947_95 Buckfast Road, Morden",SM4 5NA
U2018025,"U2018025_21 Lessness Road, Morden",SM4 6HP
U2013415,"U2013415_51 Bodnant Gardens, Raynes Park, London",SW20 0UD
U2022728,"U2022728_340 St Helier Avenue, Morden",SM4 6JU
U1009623,"U1009623_23 Pawleyne Close, Penge",SE20 8JH
U2015884,"U2015884_30 Furness Road, Morden",SM4 6PR
U2019728,"U2019728_17 Newhouse Walk, Morden",SM4 6BS
U1024719,"U1024719_14 Pawleyne Close, Penge",SE20 8JH
U2013935,"U2013935_43 Buckfast Road, Morden",SM4 5NA
U2013623,"U2013623_4 Bodmin Grove, Morden",SM4 5LU
U2018811,"U2018811_354 Lynmouth Avenue, Morden",SM4 4RT
U2019672,"U2019672_19 Netley Gardens, Morden",SM4 6JW
U2018927,"U2018927_29 Malmesbury Road, Morden",SM4 6HD
U2019237,"U2019237_37 Montacute Road, Morden",SM4 6RJ
U2019730,"U2019730_23 Newhouse Walk, Morden",SM4 6BS
U1038118,"U1038118_46 Widecombe Road, Mottingham",SE9 4HQ
U1000359,"U1000359_5 Avenue Road, Penge",SE20 7RT
U2018164,"U2018164_24 Llanthony Road, Morden",SM4 6DX
U2013982,"U2013982_18 Bury Grove, Morden",SM4 5NG
U1030964,"U1030964_13 Ravensworth Road, Mottingham",SE9 4LN
U2015882,"U2015882_25 Furness Road, Morden",SM4 6PS
U1031105,"U1031105_21 Kingsmead, Biggin Hill, Westerham",TN16 3UB
U1014008,"U1014008_9 Avenue Road, Penge",SE20 7RT
U2015779,"U2015779_57 Flanders Crescent, Tooting, London",SW17 9JB
U2013413,"U2013413_49 Bodnant Gardens, Raynes Park, London",SW20 0UD
U1021199,"U1021199_7 Dittisham Road, Mottingham",SE9 4BJ
U1041705,"U1041705_87 Seymour Villas, Penge",SE20 8TR
U1041706,"U1041706_91 Seymour Villas, Penge",SE20 8TR
U1042213,"U1042213_9 Snowdown Close, London",SE20 7RU
U2014444,"U2014444_86 Central Road, Morden",SM4 5RJ
U2013940,"U2013940_63 Buckfast Road, Morden",SM4 5NA
U1037789,"U1037789_5 Snowdown Close, London",SE20 7RU
U2013630,"U2013630_16 Bodmin Grove, Morden",SM4 5LU
U2014445,"U2014445_88 Central Road, Morden",SM4 5RJ
U2013602,"U2013602_5 Blanchland Road, Morden",SM4 5ND
U1017178,"U1017178_11 Avenue Road, Penge",SE20 7RT
U1013690,"U1013690_74 Anerley Vale, London",SE19 2BG
U2013676,"U2013676_52 Bordesley Road, Morden",SM4 5LR
U1042212,"U1042212_1 Snowdown Close, London",SE20 7RU
U2018810,"U2018810_331 Lynmouth Avenue, Morden",SM4 4RY
U2013476,"U2013476_1 Bristol Road, Morden",SM4 5SA
U1026660,"U1026660_40 Chilham Road, Mottingham",SE9 4BG
U1036877,"U1036877_2 Pawleyne Close, Penge",SE20 8JH
U2018814,"U2018814_364 Lynmouth Avenue, Morden",SM4 4RT
U2013573,"U2013573_264 Bishopsford Road, Morden",SM4 6BZ
U1007236,"U1007236_6 Castledine Road, Penge",SE20 8PL
U1027701,"U1027701_4 Wayside Grove, Mottingham",SE9 4ND
U1047208,"U1047208_83 Seymour Villas, Penge",SE20 8TR
U2013695,"U2013695_111 Bordesley Road, Morden",SM4 5LW
U1004961,"U1004961_36 Beverley Road, Penge",SE20 8SJ
U2015795,"U2015795_24 Flaxley Road, Morden",SM4 6LJ
U2014451,"U2014451_102 Central Road, Morden",SM4 5RJ
U2016606,"U2016606_3 Grayswood Gardens, Raynes Park, London",SW20 0UF
U1035025,"U1035025_29 Steyning Grove, Mottingham",SE9 4NG
U1027588,"U1027588_94 Tovil Close, Penge",SE20 8SZ
U2022632,"U2022632_112 St Helier Avenue, Morden",SM4 6LB
U1037462,"U1037462_68 Prestbury Square, Mottingham",SE9 4NA
U2018027,"U2018027_29 Lessness Road, Morden",SM4 6HP
U2018909,"U2018909_12 Malling Gardens, Morden",SM4 6JG
U1001247,"U1001247_13 Pawleyne Close, Penge",SE20 8JH
U1018547,"U1018547_9b Avenue Road, Penge",SE20 7RT
U1008780,"U1008780_7b Avenue Road, Penge",SE20 7RT
U1007212,"U1007212_2 Calcott Walk, Mottingham",SE9 4BN
U1003650,"U1003650_5b Avenue Road, Penge",SE20 7RT
U1010359,"U1010359_2 Bredhurst Close, Penge",SE20 7BG
U1041772,"13 Stanley Road, Bromley",BR2 9JE
U1011072,"45 Rookery Gardens, Oprington",BR5 4BA
U1020208,"16 Holbrook Way, Bromley",BR2 8EE
U1052580,"25 Trentham Drive, Oprington",BR5 2EP
U1038106,"84 Whitebeam Avenue, Bromley",BR2 8DW
U1010107,"42 Birch Row, Bromley",BR2 8DA
U1022307,"20 Larch Way, Bromley",BR2 8DU
U1020064,"28 Green Way, Bromley",BR2 8EY
U1023463,"30 MANOR WAY, Bromley",BR2 8ES
U1027745,"148 Whitebeam Avenue, Bromley",BR2 8DW
U1008151,"61 Lovelace Avenue, Bromley",BR2 8EA
U1019785,"50 Cray Valley Road, Oprington",BR5 2EZ
U1029229,"2 Meath Close, Oprington",BR5 2HF
U1005696,"158 Magpie Hall Lane, Bromley",BR2 8JG
U1036332,"50 Marion Crescent, Oprington",BR5 2HD
U1027830,"26 Holbrook Way, Bromley",BR2 8EE
U1052455,"10 Stiles Close, Bromley",BR2 8EQ
U1023539,"2 Marsham Close, Chislehurst",BR7 6JD
U1030962,"7 Ravensleigh Gardens, Bromley",BR1 5SN
U1037954,"28 Treewall Gardens, Bromley",BR1 5BT
U1020368,"11 Larch Way, Bromley",BR2 8DT
U1027743,"17 Whitebeam Avenue, Bromley",BR2 8DJ
U1021825,"117a Faringdon Avenue, Bromley",BR2 8BT
U1028328,"76 Magpie Hall Lane, Bromley",BR2 8ER
U1025820,"46 Princes Plain, Bromley",BR2 8LE
U1024580,"24 Parkfield Way, Bromley",BR2 8AF
U1010452,"5 Canbury Path, Oprington",BR5 2EU
U1022018,"12 Hazel Walk, Bromley",BR2 8DF
U1037814,"42 Stanley Road, Bromley",BR2 9JH
U1016744,"18 Princes Plain, Bromley",BR2 8LE
U1021310,"126 Faringdon Avenue, Bromley",BR2 8BU
U1035115,"30 Thorn Close, Bromley",BR2 8DH
U1033165,"2 Laburnum Way, Bromley",BR2 8BZ
U1000649,"42 Barnsdale Crescent, Oprington",BR5 2AX
U1052536,"12 Thorn Close, Bromley",BR2 8DH
U1042298,"39 Sudbury Crescent, Bromley",BR1 4PY
U1053388,"52 Whitebeam Avenue, Bromley",BR2 8DL
U1007556,"11 Farleigh Avenue, Bromley",BR2 7PP
U1007947,"8 Laburnum Way, Bromley",BR2 8BZ
U1019567,"8 Broom Close, Bromley",BR2 8EU
U1013358,"3 Bird In Hand Lane, Bromley",BR1 2NA
U1022533,"81 Lower Gravel Road, Bromley",BR2 8LP
U1021824,"115g Faringdon Avenue, Bromley",BR2 8BT
U1009369,"17 Minster Road, Bromley",BR1 4DY
U1022931,"15 Lennard Road, Bromley",BR2 8LN
U1024065,"64 Marion Crescent, Oprington",BR5 2HD
U1005168,"38 Holbrook Way, Bromley",BR2 8EE
U1038104,"32 Whitebeam Avenue, Bromley",BR2 8DL
U1021827,"121 Faringdon Avenue, Bromley",BR2 8BT
U1030892,"140 Poverest Road, Oprington",BR5 1RH
U1013831,"33 Ash Row, Bromley",BR2 8DZ
U1004442,"7 Birch Row, Bromley",BR2 8BX
U1019564,"32 Brook Lane, Bromley",BR1 4PU
U1032131,"93 Faringdon Avenue, Bromley",BR2 8BT
U1026960,"105 Faringdon Avenue, Bromley",BR2 8BT
U1005006,"44 Birch Row, Bromley",BR2 8DA
U1005742,"18 Malling Way, Bromley",BR2 7PJ
U1022281,"35 Kingsdown Way, Bromley",BR2 7PT
U1042574,"66 Whitebeam Avenue, Bromley",BR2 8DL
U1011191,"32 Shoreham Way, Bromley",BR2 7PU
U1032129,"78 Faringdon Avenue, Bromley",BR2 8BU
U1009032,"10 Malling Way, Bromley",BR2 7PJ
U1031121,"32 Laburnum Way, Bromley",BR2 8BZ
U1022305,"18a Lansdowne Road, Bromley",BR1 3LZ
U1042248,"16 Stanley Way, Oprington",BR5 2HE
U1038102,"14 Whitebeam Avenue, Bromley",BR2 8DL
U1007432,"21 Detling Road, Bromley",BR1 4SH
U1041934,"51 Turpington Lane, Bromley",BR2 8JA
U1032647,"32 Narrow Way, Bromley",BR2 8JB
U1052429,"76 Southover, Bromley",BR1 4RY
U1007701,"46 Harwood Avenue, Bromley",BR1 3DU
U1042572,"37 Whitebeam Avenue, Bromley",BR2 8DJ
U1042576,"112 Whitebeam Avenue, Bromley",BR2 8DW
U1020349,"21 Laburnum Way, Bromley",BR2 8BY
U1021535,"4 Chilham Way, Bromley",BR2 7PR
U1004686,"55 Baston Road, Bromley",BR2 7BD
U1027744,"33 Whitebeam Avenue, Bromley",BR2 8DJ
U1016880,"25 Almond Way, Bromley",BR2 8DR
U1023415,"27 Malling Way, Bromley",BR2 7PJ
U1011190,"11 Shoreham Way, Bromley",BR2 7PU
U1042256,"2 Stiles Close, Bromley",BR2 8EQ
U1053386,"27 Whitebeam Avenue, Bromley",BR2 8DJ
U1020237,"33 Hornbeam Way, Bromley",BR2 8DB
U1005167,"30 Holbrook Way, Bromley",BR2 8EE
U1014630,"118 Faringdon Avenue, Bromley",BR2 8BU
U1034810,"24 Newbury Road, Bromley",BR2 0QW
U1037768,"13 Silverdale Road, Oprington",BR5 2LU
U1024709,"3 Parkfield Way, Bromley",BR2 8AE
U1010582,"13 Cranworth Cottages, Keston",BR2 6DB
U1010578,"6 Cranbrook Close, Bromley",BR2 7QA
U1031058,"303 Keedonwood Road, Bromley",BR1 4QR
U1026447,"50 Princes Plain, Bromley",BR2 8LE
U1016746,"68 Princes Plain, Bromley",BR2 8LE
U1038107,"152 Whitebeam Avenue, Bromley",BR2 8DW
U1036446,"14 Meath Close, Oprington",BR5 2HF
U1014589,"71 Empress Drive, Chislehurst",BR7 5BQ
U1002456,"54 Birch Row, Bromley",BR2 8DA
U1020369,"13 Larch Way, Bromley",BR2 8DT
U1034985,"272 Southborough Lane, Bromley",BR2 8AS
U1041435,"64 Princes Plain, Bromley",BR2 8LE
U1031294,"70 London Lane, Bromley",BR1 4HE
U1033406,"67 Lower Gravel Road, Bromley",BR2 8LP
U1032061,"30 Eastry Avenue, Bromley",BR2 7PF
U1042573,"47 Whitebeam Avenue, Bromley",BR2 8DJ
U1032133,"119 Faringdon Avenue, Bromley",BR2 8BT
U1013830,"31 Ash Row, Bromley",BR2 8DZ
U1009511,"27 Newbury Road, Bromley",BR2 0QN
U1042735,"108 Whitebeam Avenue, Bromley",BR2 8DW
U1024511,"81 Nightingale Lane, Bromley",BR1 2SA
U1024050,"10 Marden Avenue, Bromley",BR2 7PX
U1035326,"2 Whitebeam Avenue, Bromley",BR2 8DL
U1009202,"63 Mead Way, Bromley",BR2 9ER
U1020199,"78 Hillside Road, Bromley",BR2 0ST
U1042232,"154 Southover, Bromley",BR1 4RZ
U1030897,"60 Princes Plain, Bromley",BR2 8LE
U1027989,"5 Larch Way, Bromley",BR2 8DT
U1021308,"70 Faringdon Avenue, Bromley",BR2 8BU
U1019906,"26 Eastry Avenue, Bromley",BR2 7PF
U1022726,"31 Hornbeam Way, Bromley",BR2 8DB
U1024698,"4 Palace View, Bromley",BR1 3EL
U1005549,"79 Lower Gravel Road, Bromley",BR2 8LP
U1052376,"3 Shoreham Way, Bromley",BR2 7PU
U1041937,"75 Turpington Lane, Bromley",BR2 8JD
U1005123,"24 Bonville Road, Bromley",BR1 4QA
U1008158,"71 Lower Gravel Road, Bromley",BR2 8LP
U1042575,"90 Whitebeam Avenue, Bromley",BR2 8DW
U1014793,"59 Headcorn Road, Bromley",BR1 4SQ
U1012309,"13 Almond Close, Bromley",BR2 8DS
U1009194,"84 Mays Hill Road, Bromley",BR2 0HT
U1027838,"15 Holmcroft Way, Bromley",BR2 8AD
U1027449,"11 Station Road, Bromley",BR1 3LP
U1014627,"81 Faringdon Avenue, Bromley",BR2 8BT
U1027493,"35 Sudbury Crescent, Bromley",BR1 4PY
U1032132,"115f Faringdon Avenue, Bromley",BR2 8BT
U1037833,"3 Stiles Close, Bromley",BR2 8EQ
U1016743,"16 Princes Plain, Bromley",BR2 8LE
U1007553,"115d Faringdon Avenue, Bromley",BR2 8BT
U1009607,"20 Parkfield Way, Bromley",BR2 8AF
U1034805,"38 Narrow Way, Bromley",BR2 8JB
U1032062,"46 Eastry Avenue, Bromley",BR2 7PF
U1047613,"13 Whitebeam Avenue, Bromley",BR2 8DJ
U1024484,"55 Mounthurst Road, Bromley",BR2 7PG
U1038103,"29 Whitebeam Avenue, Bromley",BR2 8DJ
U1022525,"30 Lovelace Avenue, Bromley",BR2 8DQ
U1042153,"4 Scotts Road, Bromley",BR1 3QD
U1037465,"6 Princes Plain, Bromley",BR2 8LE
U1041933,"31 Turpington Lane, Bromley",BR2 8JA
U1021769,"49 Eastry Avenue, Bromley",BR2 7PE
U1042734,"86 Whitebeam Avenue, Bromley",BR2 8DW
U1026958,"77 Faringdon Avenue, Bromley",BR2 8BT
U1037872,"20 Sudbury Crescent, Bromley",BR1 4PZ
U1027549,"14 Thorn Close, Bromley",BR2 8DH
U1037450,"70 Pontefract Road, Bromley",BR1 4RB
U1016742,"4 Princes Plain, Bromley",BR2 8LE
U1042733,"68 Whitebeam Avenue, Bromley",BR2 8DL
U1003906,"54 Barnsdale Crescent, Oprington",BR5 2AX
U1027392,"26 Silverdale Road, Oprington",BR5 2LT
U1052186,"4 Ravensleigh Gardens, Bromley",BR1 5SN
U1034479,"90 Mead Way, Bromley",BR2 9EU
U1005741,"2 Malling Way, Bromley",BR2 7PJ
U1024017,"13 Manor Way, Bromley",BR2 8ES
U1014078,"43 Aylesbury Road, Bromley",BR2 0QR
U1015499,"25 Boughton Avenue, Bromley",BR2 7PL
U1033177,"30 Larch Way, Bromley",BR2 8DU

1 UPRN Address Postcode
2 U1027392 U1041316 26 Silverdale Road, Oprington U1041316_58 Ravensworth Road, Mottingham BR5 2LT SE9 4LW
3 U1003906 U2022716 54 Barnsdale Crescent, Oprington U2022716_306 St Helier Avenue, Morden BR5 2AX SM4 6JX
4 U1034479 U2018001 90 Mead Way, Bromley U2018001_37 Leominster Road, Morden BR2 9EU SM4 6HN
5 U1005549 U2019131 79 Lower Gravel Road, Bromley U2019131_34 Middleton Road, Morden BR2 8LP SM4 6RT
6 U1016743 U1035331 16 Princes Plain, Bromley 104 Whitebeam Avenue, Bromley BR2 8LE BR2 8DW
7 U1041937 U1010255 75 Turpington Lane, Bromley 7 Boughton Avenue, Bromley BR2 8JD BR2 7PL
8 U1034805 U1024581 38 Narrow Way, Bromley 30 Parkfield Way, Bromley BR2 8JB BR2 8AF
9 U1041933 U1032130 31 Turpington Lane, Bromley 86 Faringdon Avenue, Bromley BR2 8JA BR2 8BU
10 U1037833 U1014077 3 Stiles Close, Bromley 32 Aylesbury Road, Bromley BR2 8EQ BR2 0QP
11 U1042734 U1007728 86 Whitebeam Avenue, Bromley 2 Hazel Walk, Bromley BR2 8DW BR2 8DF
12 U1042575 U1020327 90 Whitebeam Avenue, Bromley 5 Kingsdown Way, Bromley BR2 8DW BR2 7PT
13 U1033177 U1032134 30 Larch Way, Bromley 125 Faringdon Avenue, Bromley BR2 8DU BR2 8BT
14 U1027989 U1053411 5 Larch Way, Bromley U1053411_78 Windsor Drive, Orpington BR2 8DT BR6 6HF
15 U1012309 U2022615 13 Almond Close, Bromley U2022615_56 St Helier Avenue, Morden BR2 8DS SM4 6LF
16 U1022525 U1022991 30 Lovelace Avenue, Bromley 5 Link Way, Bromley BR2 8DQ BR2 8JH
17 U1047613 U1010811 13 Whitebeam Avenue, Bromley 13 Gilbert Road, Bromley BR2 8DJ BR1 3QP
18 U1027549 U1020328 14 Thorn Close, Bromley 31 Kingsdown Way, Bromley BR2 8DH BR2 7PT
19 U1022726 U1005131 31 Hornbeam Way, Bromley 10 Boughton Avenue, Bromley BR2 8DB BR2 7PL
20 U1021308 U1021353 70 Faringdon Avenue, Bromley 66 George Lane, Bromley BR2 8BU BR2 7LQ
21 U1026958 U1034017 77 Faringdon Avenue, Bromley 48 Princes Plain, Bromley BR2 8BT BR2 8LE
22 U1007553 U1036758 115d Faringdon Avenue, Bromley 46 Newbury Road, Bromley BR2 8BT BR2 0QW
23 U1032132 U1020351 115f Faringdon Avenue, Bromley 27 Laburnum Way, Bromley BR2 8BT BR2 8BY
24 U1014627 U1035052 81 Faringdon Avenue, Bromley 1 Sudbury Crescent, Bromley BR2 8BT BR1 4PY
25 U1009607 U1027073 20 Parkfield Way, Bromley U1027073_29 Haddon Road, St Mary Cray BR2 8AF BR5 4BU
26 U1027838 U1034887 15 Holmcroft Way, Bromley U1034887_2 Saltwood Close, Orpington BR2 8AD BR6 9BU
27 U1052376 U1035329 3 Shoreham Way, Bromley U1035329_79 Whitebeam Avenue, Bromley BR2 7PU BR2 8DN
28 U1015499 U1042571 25 Boughton Avenue, Bromley U1042571_19 Whitebeam Avenue, Bromley BR2 7PL BR2 8DJ
29 U1005741 U1035330 2 Malling Way, Bromley U1035330_87 Whitebeam Avenue, Bromley BR2 7PJ BR2 8DN
30 U1019906 U1047521 26 Eastry Avenue, Bromley U1047521_19 Walnuts Road, Orpington BR2 7PF BR6 0RG
31 U1021769 U1033037 49 Eastry Avenue, Bromley U1033037_73 Homefield Rise, Orpington BR2 7PE BR6 0RL
32 U1031121 U1027845 32 Laburnum Way, Bromley U1027845_92 Homefield Rise, Orpington BR2 8BZ BR6 0RW
33 U1022281 U1031691 35 Kingsdown Way, Bromley U1031691_46 Boyland Road, Bromley BR2 7PT BR1 4QF
34 U1035115 U1026916 30 Thorn Close, Bromley U1026916_19 Elizabeth Way, St Mary Cray BR2 8DH BR5 4BJ
35 U1027493 U1015363 35 Sudbury Crescent, Bromley U1015363_44a Birch Row, Bromley BR1 4PY BR2 8DA
36 U1016746 U1010400 68 Princes Plain, Bromley U1010400_42 Brook Lane, Bromley BR2 8LE BR1 4PU
37 U1005168 U1052426 38 Holbrook Way, Bromley U1052426_49 Southover, Bromley BR2 8EE BR1 4SL
38 U1036446 U1019591 14 Meath Close, Oprington U1019591_18 Buckland Road, Orpington BR5 2HF BR6 9SR
39 U1010452 U1011434 5 Canbury Path, Oprington U1011434_24 Upper Elmers End Road, Beckenham BR5 2EU BR3 4AL
40 U1019785 U1023417 50 Cray Valley Road, Oprington U1023417_44 Malling Way, Hayes, Bromley BR5 2EZ BR2 7PJ
41 U1024065 U1014671 64 Marion Crescent, Oprington U1014671_61 George Lane, Hayes, Bromley BR5 2HD BR2 7LG
42 U1042248 U1037550 16 Stanley Way, Oprington U1037550_228 Repton Road, Orpington BR5 2HE BR6 9JB
43 U1029229 U1014433 2 Meath Close, Oprington U1014433_57 Cowden Road, Orpington BR5 2HF BR6 0TP
44 U1037768 U1015779 13 Silverdale Road, Oprington U1015779_21 Beck Lane, Beckenham BR5 2LU BR3 4RD
45 U1014589 U1042236 71 Empress Drive, Chislehurst U1042236_250 Southover, Bromley BR7 5BQ BR1 4SE
46 U1024698 U1033036 4 Palace View, Bromley U1033036_61 Homefield Rise, Orpington BR1 3EL BR6 0RL
47 U1052536 U1027951 12 Thorn Close, Bromley U1027951_2 Kingsdown Way, Hayes, Bromley BR2 8DH BR2 7PT
48 U1022018 U1032770 12 Hazel Walk, Bromley U1032770_18 Pembury Close, Hayes, Bromley BR2 8DF BR2 7PS
49 U1007728 U1011129 2 Hazel Walk, Bromley U1011129_40 Saltwood Close, Orpington BR2 8DF BR6 9BU
50 U1002456 U1023267 54 Birch Row, Bromley U1023267_47a Mackenzie Road, Beckenham BR2 8DA BR3 4RT
51 U1020349 U1016998 21 Laburnum Way, Bromley U1016998_68 Arundel Drive, Orpington BR2 8BY BR6 9JG
52 U1032129 U1026576 78 Faringdon Avenue, Bromley U1026576_1 Burwash Court, St Mary Cray BR2 8BU BR5 4BG
53 U1032130 U1037867 86 Faringdon Avenue, Bromley U1037867_45 Stowe Road, Orpington BR2 8BU BR6 9HG
54 U1021824 U1027839 115g Faringdon Avenue, Bromley U1027839_30 Holmcroft Way, Bromley BR2 8BT BR2 8AD
55 U1021827 U1005351 121 Faringdon Avenue, Bromley U1005351_2 Leaveland Close, Beckenham BR2 8BT BR3 3PL
56 U1026960 U1041968 105 Faringdon Avenue, Bromley U1041968_59 Uplands Road, Orpington BR2 8BT BR6 0RH
57 U1010578 U1032149 6 Cranbrook Close, Bromley U1032149_12 Fordwich Close, Orpington BR2 7QA BR6 0TT
58 U1024709 U1047347 3 Parkfield Way, Bromley U1047347_30 Sudbury Crescent, Bromley BR2 8AE BR1 4PZ
59 U1024580 U1004249 24 Parkfield Way, Bromley U1004249_6 Beck Lane, Beckenham BR2 8AF BR3 4RE
60 U1011190 U1005772 11 Shoreham Way, Bromley U1005772_10 Manor Park Road, West Wickham BR2 7PU BR4 0JZ
61 U1011191 U1010810 32 Shoreham Way, Bromley U1010810_79 George Lane, Hayes, Bromley BR2 7PU BR2 7LG
62 U1021535 U1011008 4 Chilham Way, Bromley U1011008_170 Repton Road, Orpington BR2 7PR BR6 9JA
63 U1007556 U1026969 11 Farleigh Avenue, Bromley U1026969_2 Fordwich Close, Orpington BR2 7PP BR6 0TT
64 U1010255 U1032000 7 Boughton Avenue, Bromley U1032000_16 Detling Road, Bromley BR2 7PL BR1 4SH
65 U1034810 U1013268 24 Newbury Road, Bromley U1013268_9 Bell Gardens, St Mary Cray BR2 0QW BR5 4DA
66 U1009032 U1020217 10 Malling Way, Bromley U1020217_71 Homefield Rise, Orpington BR2 7PJ BR6 0RL
67 U1004686 U1033287 55 Baston Road, Bromley U1033287_12 Lincoln Green Road, St Mary Cray BR2 7BD BR5 2DX
68 U1032061 U1041997 30 Eastry Avenue, Bromley U1041997_31 Walnuts Road, Orpington BR2 7PF BR6 0RG
69 U1010582 U1047520 13 Cranworth Cottages, Keston U1047520_2 Walnuts Road, Orpington BR2 6DB BR6 0RQ
70 U1009511 U1019606 27 Newbury Road, Bromley U1019606_20 Burrfield Drive, St Mary Cray BR2 0QN BR5 4BZ
71 U1037954 U1007354 28 Treewall Gardens, Bromley U1007354_30 Cowden Road, Orpington BR1 5BT BR6 0TR
72 U1014793 U1015603 59 Headcorn Road, Bromley U1015603_1 Barwood Avenue, West Wickham BR1 4SQ BR4 0LB
73 U1005123 U1035393 24 Bonville Road, Bromley U1035393_18 Yalding Grove, St Mary Cray BR1 4QA BR5 3SE
74 U1037872 U1000177 20 Sudbury Crescent, Bromley U1000177_79 Arundel Drive, Orpington BR1 4PZ BR6 9JQ
75 U1022305 U1011428 18a Lansdowne Road, Bromley U1011428_21 Uplands Road, Orpington BR1 3LZ BR6 0RH
76 U1035052 U1024671 1 Sudbury Crescent, Bromley U1024671_115 Oregon Square, Orpington BR1 4PY BR6 8BE
77 U1042298 U1021513 39 Sudbury Crescent, Bromley U1021513_18 Chamberlain Crescent, West Wickham BR1 4PY BR4 0LL
78 U1019564 U1024663 32 Brook Lane, Bromley U1024663_32b Okemore Gardens, Orpington BR1 4PU BR5 3PJ
79 U1024511 U1020879 81 Nightingale Lane, Bromley U1020879_8 Brockwell Close, St Mary Cray BR1 2SA BR5 1RJ
80 U1032133 U1022713 119 Faringdon Avenue, Bromley U1022713_53 Homefield Rise, Orpington BR2 8BT BR6 0RL
81 U1032134 U1031157 125 Faringdon Avenue, Bromley U1031157_94 Leesons Hill, St Pauls Cray BR2 8BT BR5 2NH
82 U1032131 U1013666 93 Faringdon Avenue, Bromley U1013666_11 Ampleforth Close, Orpington BR2 8BT BR6 9ES
83 U1021825 U1052617 117a Faringdon Avenue, Bromley U1052617_44 Uplands Road, Orpington BR2 8BT BR6 0RJ
84 U1010107 U1010783 42 Birch Row, Bromley U1010783_32 Foxbury Drive, Chelsfield, Orpington BR2 8DA BR6 6EL
85 U1016880 U1021700 25 Almond Way, Bromley U1021700_25 Daleside, Orpington BR2 8DR BR6 6EG
86 U1038107 U1011163 152 Whitebeam Avenue, Bromley U1011163_35 Selwyn Place, St Pauls Cray BR2 8DW BR5 3BA
87 U1052455 U1022175 10 Stiles Close, Bromley U1022175_54 Homefield Rise, Orpington BR2 8EQ BR6 0RW
88 U1028328 U1021461 76 Magpie Hall Lane, Bromley U1021461_27 Burrfield Drive, St Mary Cray BR2 8ER BR5 4BZ
89 U1020064 U1007184 28 Green Way, Bromley U1007184_7 Buckland Road, Orpington BR2 8EY BR6 9SR
90 U1041934 U1023112 51 Turpington Lane, Bromley U1023112_97 Lower Gravel Road, Bromley BR2 8JA BR2 8LP
91 U1005696 U1024516 158 Magpie Hall Lane, Bromley U1024516_18 Northolme Rise, Off Tubbenden Lane, Orpington BR2 8JG BR6 9RF
92 U1030892 U1023910 140 Poverest Road, Oprington U1023910_40 Moorfield Road, Orpington BR5 1RH BR6 0HQ
93 U1011072 U1031342 45 Rookery Gardens, Oprington U1031342_55 Lower Gravel Road, Bromley BR5 4BA BR2 8LP
94 U1031058 U1037905 303 Keedonwood Road, Bromley U1037905_37 Sweeps Lane, St Mary Cray BR1 4QR BR5 3PE
95 U1052429 U2019260 76 Southover, Bromley U2019260_88 Montacute Road, Morden BR1 4RY SM4 6RL
96 U1042153 U2019214 4 Scotts Road, Bromley U2019214_11 Missenden Gardens, Morden BR1 3QD SM4 6HW
97 U1037814 U2022891 42 Stanley Road, Bromley U2022891_20 Swains Road, Tooting, London BR2 9JH SW17 9HS
98 U1008158 U2022684 71 Lower Gravel Road, Bromley U2022684_231 St Helier Avenue, Morden BR2 8LP SM4 6JH
99 U1032062 U2023196 46 Eastry Avenue, Bromley U2023196_210 Tudor Drive, Morden BR2 7PF SM4 4PQ
100 U1016742 U2019671 4 Princes Plain, Bromley U2019671_16 Netley Gardens, Morden BR2 8LE SM4 6JW
101 U1038106 U2018944 84 Whitebeam Avenue, Bromley U2018944_79 Malmesbury Road, Morden BR2 8DW SM4 6HG
102 U1013831 U2015873 33 Ash Row, Bromley U2015873_1 Furness Road, Morden BR2 8DZ SM4 6PS
103 U1005742 U1041503 18 Malling Way, Bromley U1041503_103 Ravensworth Road, Mottingham BR2 7PJ SE9 4LX
104 U1042572 U2023185 37 Whitebeam Avenue, Bromley U2023185_157 Tudor Drive, Morden BR2 8DJ SM4 4PJ
105 U1005167 U1053395 30 Holbrook Way, Bromley U1053395_90 Widecombe Road, Mottingham BR2 8EE SE9 4HJ
106 U1024581 U2018009 30 Parkfield Way, Bromley U2018009_23 Leominster Walk, Morden BR2 8AF SM4 6HB
107 U1024050 U2018941 10 Marden Avenue, Bromley U2018941_74 Malmesbury Road, Morden BR2 7PX SM4 6HE
108 U1020328 U2013950 31 Kingsdown Way, Bromley U2013950_2 Buckland Walk, Morden BR2 7PT SM4 5RZ
109 U1020327 U2018160 5 Kingsdown Way, Bromley U2018160_6 Llanthony Road, Morden BR2 7PT SM4 6DX
110 U1005131 U2015790 10 Boughton Avenue, Bromley U2015790_12 Flaxley Road, Morden BR2 7PL SM4 6LJ
111 U1023415 U2013914 27 Malling Way, Bromley U2013914_36 Bruton Road, Morden BR2 7PJ SM4 5RY
112 U1052580 U2022611 25 Trentham Drive, Oprington U2022611_50 St Helier Avenue, Morden BR5 2EP SM4 6LF
113 U1026447 U2015270 50 Princes Plain, Bromley U2015270_39 Dorchester Road, Morden BR2 8LE SM4 6QE
114 U1007432 U1009098 21 Detling Road, Bromley U1009098_37b Maple Road, Penge BR1 4SH SE20 8LA
115 U1053388 U2018926 52 Whitebeam Avenue, Bromley U2018926_19 Malmesbury Road, Morden BR2 8DL SM4 6HD
116 U1022307 U2023192 20 Larch Way, Bromley U2023192_195 Tudor Drive, Morden BR2 8DU SM4 4PJ
117 U1027743 U1027362 17 Whitebeam Avenue, Bromley U1027362_187 Seymour Villas, Penge BR2 8DJ SE20 8TP
118 U1022533 U2018973 81 Lower Gravel Road, Bromley U2018973_26 Marham Gardens, Morden BR2 8LP SM4 6JJ
119 U1014630 U2013913 118 Faringdon Avenue, Bromley U2013913_30 Bruton Road, Morden BR2 8BU SM4 5RY
120 U1030897 U1042214 60 Princes Plain, Bromley U1042214_25 Snowdown Close, London BR2 8LE SE20 7RU
121 U1022931 U2013911 15 Lennard Road, Bromley U2013911_23 Bruton Road, Morden BR2 8LN SM4 5RY
122 U1042735 U2013583 108 Whitebeam Avenue, Bromley U2013583_300 Bishopsford Road, Morden BR2 8DW SM4 6BY
123 U1013830 U2018925 31 Ash Row, Bromley U2018925_14 Malmesbury Road, Morden BR2 8DZ SM4 6HD
124 U1020368 U1037938 11 Larch Way, Bromley U1037938_5 Torr Road, Penge BR2 8DT SE20 7PS
125 U1020369 U1038117 13 Larch Way, Bromley U1038117_40 Widecombe Road, Mottingham BR2 8DT SE9 4HQ
126 U1042576 U1052365 112 Whitebeam Avenue, Bromley U1052365_127 Seymour Villas, Penge BR2 8DW SE20 8TP
127 U1027830 U1024726 26 Holbrook Way, Bromley U1024726_2 Penge Lane, Penge BR2 8EE SE20 7DU
128 U1016744 U1026505 18 Princes Plain, Bromley U1026505_7 Bridlington Close, Biggin Hill BR2 8LE TN16 3PD
129 U1041772 U2018913 13 Stanley Road, Bromley U2018913_23 Malling Gardens, Morden BR2 9JE SM4 6JG
130 U1042573 U2015708 47 Whitebeam Avenue, Bromley U2015708_46 Faversham Road, Morden BR2 8DJ SM4 6RE
131 U1008151 U2019530 61 Lovelace Avenue, Bromley U2019530_30 Neath Gardens, Morden BR2 8EA SM4 6JN
132 U1034985 U2015710 272 Southborough Lane, Bromley U2015710_49 Faversham Road, Morden BR2 8AS SM4 6RE
133 U1007947 U2014478 8 Laburnum Way, Bromley U2014478_222 Central Road, Morden BR2 8BZ SM4 5PP
134 U1027744 U2018922 33 Whitebeam Avenue, Bromley U2018922_11 Malmesbury Road, Morden BR2 8DJ SM4 6HD
135 U1038102 U2013615 14 Whitebeam Avenue, Bromley U2013615_47 Blanchland Road, Morden BR2 8DL SM4 5ND
136 U1027745 U2022641 148 Whitebeam Avenue, Bromley U2022641_127 St Helier Avenue, Morden BR2 8DW SM4 6JD
137 U1020208 U2019268 16 Holbrook Way, Bromley U2019268_97 Montacute Road, Morden BR2 8EE SM4 6RJ
138 U1023463 U2013947 30 MANOR WAY, Bromley U2013947_95 Buckfast Road, Morden BR2 8ES SM4 5NA
139 U1032647 U2018025 32 Narrow Way, Bromley U2018025_21 Lessness Road, Morden BR2 8JB SM4 6HP
140 U1033406 U2013415 67 Lower Gravel Road, Bromley U2013415_51 Bodnant Gardens, Raynes Park, London BR2 8LP SW20 0UD
141 U1000649 U2022728 42 Barnsdale Crescent, Oprington U2022728_340 St Helier Avenue, Morden BR5 2AX SM4 6JU
142 U1036332 U1009623 50 Marion Crescent, Oprington U1009623_23 Pawleyne Close, Penge BR5 2HD SE20 8JH
143 U1038103 U2015884 29 Whitebeam Avenue, Bromley U2015884_30 Furness Road, Morden BR2 8DJ SM4 6PR
144 U1042574 U2019728 66 Whitebeam Avenue, Bromley U2019728_17 Newhouse Walk, Morden BR2 8DL SM4 6BS
145 U1042256 U1024719 2 Stiles Close, Bromley U1024719_14 Pawleyne Close, Penge BR2 8EQ SE20 8JH
146 U1034017 U2013935 48 Princes Plain, Bromley U2013935_43 Buckfast Road, Morden BR2 8LE SM4 5NA
147 U1041435 U2013623 64 Princes Plain, Bromley U2013623_4 Bodmin Grove, Morden BR2 8LE SM4 5LU
148 U1053386 U2018811 27 Whitebeam Avenue, Bromley U2018811_354 Lynmouth Avenue, Morden BR2 8DJ SM4 4RT
149 U1038104 U2019672 32 Whitebeam Avenue, Bromley U2019672_19 Netley Gardens, Morden BR2 8DL SM4 6JW
150 U1019567 U2018927 8 Broom Close, Bromley U2018927_29 Malmesbury Road, Morden BR2 8EU SM4 6HD
151 U1023539 U2019237 2 Marsham Close, Chislehurst U2019237_37 Montacute Road, Morden BR7 6JD SM4 6RJ
152 U1037465 U2019730 6 Princes Plain, Bromley U2019730_23 Newhouse Walk, Morden BR2 8LE SM4 6BS
153 U1042733 U1038118 68 Whitebeam Avenue, Bromley U1038118_46 Widecombe Road, Mottingham BR2 8DL SE9 4HQ
154 U1004442 U1000359 7 Birch Row, Bromley U1000359_5 Avenue Road, Penge BR2 8BX SE20 7RT
155 U1005006 U2018164 44 Birch Row, Bromley U2018164_24 Llanthony Road, Morden BR2 8DA SM4 6DX
156 U1035331 U2013982 104 Whitebeam Avenue, Bromley U2013982_18 Bury Grove, Morden BR2 8DW SM4 5NG
157 U1024017 U1030964 13 Manor Way, Bromley U1030964_13 Ravensworth Road, Mottingham BR2 8ES SE9 4LN
158 U1014078 U2015882 43 Aylesbury Road, Bromley U2015882_25 Furness Road, Morden BR2 0QR SM4 6PS
159 U1052186 U1031105 4 Ravensleigh Gardens, Bromley U1031105_21 Kingsmead, Biggin Hill, Westerham BR1 5SN TN16 3UB
160 U1037450 U1014008 70 Pontefract Road, Bromley U1014008_9 Avenue Road, Penge BR1 4RB SE20 7RT
161 U1031294 U2015779 70 London Lane, Bromley U2015779_57 Flanders Crescent, Tooting, London BR1 4HE SW17 9JB
162 U1014077 U2013413 32 Aylesbury Road, Bromley U2013413_49 Bodnant Gardens, Raynes Park, London BR2 0QP SW20 0UD
163 U1030962 U1021199 7 Ravensleigh Gardens, Bromley U1021199_7 Dittisham Road, Mottingham BR1 5SN SE9 4BJ
164 U1042232 U1041705 154 Southover, Bromley U1041705_87 Seymour Villas, Penge BR1 4RZ SE20 8TR
165 U1024484 U1041706 55 Mounthurst Road, Bromley U1041706_91 Seymour Villas, Penge BR2 7PG SE20 8TR
166 U1007701 U1042213 46 Harwood Avenue, Bromley U1042213_9 Snowdown Close, London BR1 3DU SE20 7RU
167 U1020199 U2014444 78 Hillside Road, Bromley U2014444_86 Central Road, Morden BR2 0ST SM4 5RJ
168 U1036758 U2013940 46 Newbury Road, Bromley U2013940_63 Buckfast Road, Morden BR2 0QW SM4 5NA
169 U1009369 U1037789 17 Minster Road, Bromley U1037789_5 Snowdown Close, London BR1 4DY SE20 7RU
170 U1009194 U2013630 84 Mays Hill Road, Bromley U2013630_16 Bodmin Grove, Morden BR2 0HT SM4 5LU
171 U1013358 U2014445 3 Bird In Hand Lane, Bromley U2014445_88 Central Road, Morden BR1 2NA SM4 5RJ
172 U1009202 U2013602 63 Mead Way, Bromley U2013602_5 Blanchland Road, Morden BR2 9ER SM4 5ND
173 U1022991 U1017178 5 Link Way, Bromley U1017178_11 Avenue Road, Penge BR2 8JH SE20 7RT
174 U1025820 U1013690 46 Princes Plain, Bromley U1013690_74 Anerley Vale, London BR2 8LE SE19 2BG
175 U1020237 U2013676 33 Hornbeam Way, Bromley U2013676_52 Bordesley Road, Morden BR2 8DB SM4 5LR
176 U1021310 U1042212 126 Faringdon Avenue, Bromley U1042212_1 Snowdown Close, London BR2 8BU SE20 7RU
177 U1021353 U2018810 66 George Lane, Bromley U2018810_331 Lynmouth Avenue, Morden BR2 7LQ SM4 4RY
178 U1033165 U2013476 2 Laburnum Way, Bromley U2013476_1 Bristol Road, Morden BR2 8BZ SM4 5SA
179 U1010811 U1026660 13 Gilbert Road, Bromley U1026660_40 Chilham Road, Mottingham BR1 3QP SE9 4BG
180 U1027449 U1036877 11 Station Road, Bromley U1036877_2 Pawleyne Close, Penge BR1 3LP SE20 8JH
181 U1035326 U2018814 2 Whitebeam Avenue, Bromley U2018814_364 Lynmouth Avenue, Morden BR2 8DL SM4 4RT
182 U1020351 U2013573 27 Laburnum Way, Bromley U2013573_264 Bishopsford Road, Morden BR2 8BY SM4 6BZ
183 U1007236 U1007236_6 Castledine Road, Penge SE20 8PL
184 U1027701 U1027701_4 Wayside Grove, Mottingham SE9 4ND
185 U1047208 U1047208_83 Seymour Villas, Penge SE20 8TR
186 U2013695 U2013695_111 Bordesley Road, Morden SM4 5LW
187 U1004961 U1004961_36 Beverley Road, Penge SE20 8SJ
188 U2015795 U2015795_24 Flaxley Road, Morden SM4 6LJ
189 U2014451 U2014451_102 Central Road, Morden SM4 5RJ
190 U2016606 U2016606_3 Grayswood Gardens, Raynes Park, London SW20 0UF
191 U1035025 U1035025_29 Steyning Grove, Mottingham SE9 4NG
192 U1027588 U1027588_94 Tovil Close, Penge SE20 8SZ
193 U2022632 U2022632_112 St Helier Avenue, Morden SM4 6LB
194 U1037462 U1037462_68 Prestbury Square, Mottingham SE9 4NA
195 U2018027 U2018027_29 Lessness Road, Morden SM4 6HP
196 U2018909 U2018909_12 Malling Gardens, Morden SM4 6JG
197 U1001247 U1001247_13 Pawleyne Close, Penge SE20 8JH
198 U1018547 U1018547_9b Avenue Road, Penge SE20 7RT
199 U1008780 U1008780_7b Avenue Road, Penge SE20 7RT
200 U1007212 U1007212_2 Calcott Walk, Mottingham SE9 4BN
201 U1003650 U1003650_5b Avenue Road, Penge SE20 7RT
202 U1010359 U1010359_2 Bredhurst Close, Penge SE20 7BG
203 U1041772 13 Stanley Road, Bromley BR2 9JE
204 U1011072 45 Rookery Gardens, Oprington BR5 4BA
205 U1020208 16 Holbrook Way, Bromley BR2 8EE
206 U1052580 25 Trentham Drive, Oprington BR5 2EP
207 U1038106 84 Whitebeam Avenue, Bromley BR2 8DW
208 U1010107 42 Birch Row, Bromley BR2 8DA
209 U1022307 20 Larch Way, Bromley BR2 8DU
210 U1020064 28 Green Way, Bromley BR2 8EY
211 U1023463 30 MANOR WAY, Bromley BR2 8ES
212 U1027745 148 Whitebeam Avenue, Bromley BR2 8DW
213 U1008151 61 Lovelace Avenue, Bromley BR2 8EA
214 U1019785 50 Cray Valley Road, Oprington BR5 2EZ
215 U1029229 2 Meath Close, Oprington BR5 2HF
216 U1005696 158 Magpie Hall Lane, Bromley BR2 8JG
217 U1036332 50 Marion Crescent, Oprington BR5 2HD
218 U1027830 26 Holbrook Way, Bromley BR2 8EE
219 U1052455 10 Stiles Close, Bromley BR2 8EQ
220 U1023539 2 Marsham Close, Chislehurst BR7 6JD
221 U1030962 7 Ravensleigh Gardens, Bromley BR1 5SN
222 U1037954 28 Treewall Gardens, Bromley BR1 5BT
223 U1020368 11 Larch Way, Bromley BR2 8DT
224 U1027743 17 Whitebeam Avenue, Bromley BR2 8DJ
225 U1021825 117a Faringdon Avenue, Bromley BR2 8BT
226 U1028328 76 Magpie Hall Lane, Bromley BR2 8ER
227 U1025820 46 Princes Plain, Bromley BR2 8LE
228 U1024580 24 Parkfield Way, Bromley BR2 8AF
229 U1010452 5 Canbury Path, Oprington BR5 2EU
230 U1022018 12 Hazel Walk, Bromley BR2 8DF
231 U1037814 42 Stanley Road, Bromley BR2 9JH
232 U1016744 18 Princes Plain, Bromley BR2 8LE
233 U1021310 126 Faringdon Avenue, Bromley BR2 8BU
234 U1035115 30 Thorn Close, Bromley BR2 8DH
235 U1033165 2 Laburnum Way, Bromley BR2 8BZ
236 U1000649 42 Barnsdale Crescent, Oprington BR5 2AX
237 U1052536 12 Thorn Close, Bromley BR2 8DH
238 U1042298 39 Sudbury Crescent, Bromley BR1 4PY
239 U1053388 52 Whitebeam Avenue, Bromley BR2 8DL
240 U1007556 11 Farleigh Avenue, Bromley BR2 7PP
241 U1007947 8 Laburnum Way, Bromley BR2 8BZ
242 U1019567 8 Broom Close, Bromley BR2 8EU
243 U1013358 3 Bird In Hand Lane, Bromley BR1 2NA
244 U1022533 81 Lower Gravel Road, Bromley BR2 8LP
245 U1021824 115g Faringdon Avenue, Bromley BR2 8BT
246 U1009369 17 Minster Road, Bromley BR1 4DY
247 U1022931 15 Lennard Road, Bromley BR2 8LN
248 U1024065 64 Marion Crescent, Oprington BR5 2HD
249 U1005168 38 Holbrook Way, Bromley BR2 8EE
250 U1038104 32 Whitebeam Avenue, Bromley BR2 8DL
251 U1021827 121 Faringdon Avenue, Bromley BR2 8BT
252 U1030892 140 Poverest Road, Oprington BR5 1RH
253 U1013831 33 Ash Row, Bromley BR2 8DZ
254 U1004442 7 Birch Row, Bromley BR2 8BX
255 U1019564 32 Brook Lane, Bromley BR1 4PU
256 U1032131 93 Faringdon Avenue, Bromley BR2 8BT
257 U1026960 105 Faringdon Avenue, Bromley BR2 8BT
258 U1005006 44 Birch Row, Bromley BR2 8DA
259 U1005742 18 Malling Way, Bromley BR2 7PJ
260 U1022281 35 Kingsdown Way, Bromley BR2 7PT
261 U1042574 66 Whitebeam Avenue, Bromley BR2 8DL
262 U1011191 32 Shoreham Way, Bromley BR2 7PU
263 U1032129 78 Faringdon Avenue, Bromley BR2 8BU
264 U1009032 10 Malling Way, Bromley BR2 7PJ
265 U1031121 32 Laburnum Way, Bromley BR2 8BZ
266 U1022305 18a Lansdowne Road, Bromley BR1 3LZ
267 U1042248 16 Stanley Way, Oprington BR5 2HE
268 U1038102 14 Whitebeam Avenue, Bromley BR2 8DL
269 U1007432 21 Detling Road, Bromley BR1 4SH
270 U1041934 51 Turpington Lane, Bromley BR2 8JA
271 U1032647 32 Narrow Way, Bromley BR2 8JB
272 U1052429 76 Southover, Bromley BR1 4RY
273 U1007701 46 Harwood Avenue, Bromley BR1 3DU
274 U1042572 37 Whitebeam Avenue, Bromley BR2 8DJ
275 U1042576 112 Whitebeam Avenue, Bromley BR2 8DW
276 U1020349 21 Laburnum Way, Bromley BR2 8BY
277 U1021535 4 Chilham Way, Bromley BR2 7PR
278 U1004686 55 Baston Road, Bromley BR2 7BD
279 U1027744 33 Whitebeam Avenue, Bromley BR2 8DJ
280 U1016880 25 Almond Way, Bromley BR2 8DR
281 U1023415 27 Malling Way, Bromley BR2 7PJ
282 U1011190 11 Shoreham Way, Bromley BR2 7PU
283 U1042256 2 Stiles Close, Bromley BR2 8EQ
284 U1053386 27 Whitebeam Avenue, Bromley BR2 8DJ
285 U1020237 33 Hornbeam Way, Bromley BR2 8DB
286 U1005167 30 Holbrook Way, Bromley BR2 8EE
287 U1014630 118 Faringdon Avenue, Bromley BR2 8BU
288 U1034810 24 Newbury Road, Bromley BR2 0QW
289 U1037768 13 Silverdale Road, Oprington BR5 2LU
290 U1024709 3 Parkfield Way, Bromley BR2 8AE
291 U1010582 13 Cranworth Cottages, Keston BR2 6DB
292 U1010578 6 Cranbrook Close, Bromley BR2 7QA
293 U1031058 303 Keedonwood Road, Bromley BR1 4QR
294 U1026447 50 Princes Plain, Bromley BR2 8LE
295 U1016746 68 Princes Plain, Bromley BR2 8LE
296 U1038107 152 Whitebeam Avenue, Bromley BR2 8DW
297 U1036446 14 Meath Close, Oprington BR5 2HF
298 U1014589 71 Empress Drive, Chislehurst BR7 5BQ
299 U1002456 54 Birch Row, Bromley BR2 8DA
300 U1020369 13 Larch Way, Bromley BR2 8DT
301 U1034985 272 Southborough Lane, Bromley BR2 8AS
302 U1041435 64 Princes Plain, Bromley BR2 8LE
303 U1031294 70 London Lane, Bromley BR1 4HE
304 U1033406 67 Lower Gravel Road, Bromley BR2 8LP
305 U1032061 30 Eastry Avenue, Bromley BR2 7PF
306 U1042573 47 Whitebeam Avenue, Bromley BR2 8DJ
307 U1032133 119 Faringdon Avenue, Bromley BR2 8BT
308 U1013830 31 Ash Row, Bromley BR2 8DZ
309 U1009511 27 Newbury Road, Bromley BR2 0QN
310 U1042735 108 Whitebeam Avenue, Bromley BR2 8DW
311 U1024511 81 Nightingale Lane, Bromley BR1 2SA
312 U1024050 10 Marden Avenue, Bromley BR2 7PX
313 U1035326 2 Whitebeam Avenue, Bromley BR2 8DL
314 U1009202 63 Mead Way, Bromley BR2 9ER
315 U1020199 78 Hillside Road, Bromley BR2 0ST
316 U1042232 154 Southover, Bromley BR1 4RZ
317 U1030897 60 Princes Plain, Bromley BR2 8LE
318 U1027989 5 Larch Way, Bromley BR2 8DT
319 U1021308 70 Faringdon Avenue, Bromley BR2 8BU
320 U1019906 26 Eastry Avenue, Bromley BR2 7PF
321 U1022726 31 Hornbeam Way, Bromley BR2 8DB
322 U1024698 4 Palace View, Bromley BR1 3EL
323 U1005549 79 Lower Gravel Road, Bromley BR2 8LP
324 U1052376 3 Shoreham Way, Bromley BR2 7PU
325 U1041937 75 Turpington Lane, Bromley BR2 8JD
326 U1005123 24 Bonville Road, Bromley BR1 4QA
327 U1008158 71 Lower Gravel Road, Bromley BR2 8LP
328 U1042575 90 Whitebeam Avenue, Bromley BR2 8DW
329 U1014793 59 Headcorn Road, Bromley BR1 4SQ
330 U1012309 13 Almond Close, Bromley BR2 8DS
331 U1009194 84 Mays Hill Road, Bromley BR2 0HT
332 U1027838 15 Holmcroft Way, Bromley BR2 8AD
333 U1027449 11 Station Road, Bromley BR1 3LP
334 U1014627 81 Faringdon Avenue, Bromley BR2 8BT
335 U1027493 35 Sudbury Crescent, Bromley BR1 4PY
336 U1032132 115f Faringdon Avenue, Bromley BR2 8BT
337 U1037833 3 Stiles Close, Bromley BR2 8EQ
338 U1016743 16 Princes Plain, Bromley BR2 8LE
339 U1007553 115d Faringdon Avenue, Bromley BR2 8BT
340 U1009607 20 Parkfield Way, Bromley BR2 8AF
341 U1034805 38 Narrow Way, Bromley BR2 8JB
342 U1032062 46 Eastry Avenue, Bromley BR2 7PF
343 U1047613 13 Whitebeam Avenue, Bromley BR2 8DJ
344 U1024484 55 Mounthurst Road, Bromley BR2 7PG
345 U1038103 29 Whitebeam Avenue, Bromley BR2 8DJ
346 U1022525 30 Lovelace Avenue, Bromley BR2 8DQ
347 U1042153 4 Scotts Road, Bromley BR1 3QD
348 U1037465 6 Princes Plain, Bromley BR2 8LE
349 U1041933 31 Turpington Lane, Bromley BR2 8JA
350 U1021769 49 Eastry Avenue, Bromley BR2 7PE
351 U1042734 86 Whitebeam Avenue, Bromley BR2 8DW
352 U1026958 77 Faringdon Avenue, Bromley BR2 8BT
353 U1037872 20 Sudbury Crescent, Bromley BR1 4PZ
354 U1027549 14 Thorn Close, Bromley BR2 8DH
355 U1037450 70 Pontefract Road, Bromley BR1 4RB
356 U1016742 4 Princes Plain, Bromley BR2 8LE
357 U1042733 68 Whitebeam Avenue, Bromley BR2 8DL
358 U1003906 54 Barnsdale Crescent, Oprington BR5 2AX
359 U1027392 26 Silverdale Road, Oprington BR5 2LT
360 U1052186 4 Ravensleigh Gardens, Bromley BR1 5SN
361 U1034479 90 Mead Way, Bromley BR2 9EU
362 U1005741 2 Malling Way, Bromley BR2 7PJ
363 U1024017 13 Manor Way, Bromley BR2 8ES
364 U1014078 43 Aylesbury Road, Bromley BR2 0QR
365 U1015499 25 Boughton Avenue, Bromley BR2 7PL
366 U1033177 30 Larch Way, Bromley BR2 8DU

View file

@ -1,2 +0,0 @@
UPRN,Address,Postcode
U1014630,"118 Faringdon Avenue, Bromley",BR2 8BU
1 UPRN Address Postcode
2 U1014630 118 Faringdon Avenue, Bromley BR2 8BU

View file

@ -23,6 +23,7 @@ class HubspotDealData(SQLModel, table=True):
landlord_property_id: Optional[str] = Field(default=None)
uprn: Optional[str] = Field(default=None)
outcome: Optional[str] = Field(default=None)
number_of_attempts: Optional[str] = Field(default=None)
outcome_notes: Optional[str] = Field(default=None)
booking_status: Optional[str] = Field(default=None)

View file

@ -5,7 +5,10 @@ from datetime import date
import pytest
from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor
from datatypes.epc.domain.epc_property_data import BuildingPartIdentifier, EpcPropertyData
from datatypes.epc.domain.epc_property_data import (
BuildingPartIdentifier,
EpcPropertyData,
)
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
FIXTURE_PATH = os.path.join(
@ -179,10 +182,15 @@ class TestBuildingPart:
assert len(result.sap_building_parts[0].sap_floor_dimensions) == 1
def test_floor_dimension_area(self, result: EpcPropertyData) -> None:
assert result.sap_building_parts[0].sap_floor_dimensions[0].total_floor_area_m2 == 44.89
assert (
result.sap_building_parts[0].sap_floor_dimensions[0].total_floor_area_m2
== 44.89
)
def test_floor_dimension_room_height(self, result: EpcPropertyData) -> None:
assert result.sap_building_parts[0].sap_floor_dimensions[0].room_height_m == 2.24
assert (
result.sap_building_parts[0].sap_floor_dimensions[0].room_height_m == 2.24
)
def test_floor_dimension_heat_loss_perimeter(self, result: EpcPropertyData) -> None:
assert (
@ -245,11 +253,17 @@ class TestWindows:
def test_transmission_solar_transmittance(self, result: EpcPropertyData) -> None:
assert result.sap_windows[0].window_transmission_details is not None
assert result.sap_windows[0].window_transmission_details.solar_transmittance == 0.72
assert (
result.sap_windows[0].window_transmission_details.solar_transmittance
== 0.72
)
def test_transmission_data_source(self, result: EpcPropertyData) -> None:
assert result.sap_windows[0].window_transmission_details is not None
assert result.sap_windows[0].window_transmission_details.data_source == "Manufacturer"
assert (
result.sap_windows[0].window_transmission_details.data_source
== "Manufacturer"
)
class TestHeating:
@ -308,7 +322,7 @@ class TestHeating:
class TestEnergySource:
def test_mains_gas(self, result: EpcPropertyData) -> None:
assert result.sap_energy_source.mains_gas is True
assert result.sap_energy_source.gas_connection_available is True
def test_meter_type(self, result: EpcPropertyData) -> None:
assert result.sap_energy_source.meter_type == "Single"
@ -384,5 +398,7 @@ class TestWindowFrameMaterial:
class TestLowEnergyLighting:
def test_low_energy_fixed_lighting_bulbs_count(self, result2: EpcPropertyData) -> None:
def test_low_energy_fixed_lighting_bulbs_count(
self, result2: EpcPropertyData
) -> None:
assert result2.low_energy_fixed_lighting_bulbs_count == 5

View file

@ -177,7 +177,7 @@ class TestPdfToEpcPropertyData:
),
],
sap_energy_source=SapEnergySource(
mains_gas=True,
gas_connection_available=True,
meter_type="Single",
pv_battery_count=0,
wind_turbines_count=0,

View file

@ -0,0 +1,368 @@
{
"uprn": 100020942571,
"roofs": [
{
"description": "(another dwelling above)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"walls": [
{
"description": "System built, with internal insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "Solid, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
{
"description": "To external air, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 1,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"lighting": {
"description": "Low energy lighting in all fixed outlets",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "SE28 8BW",
"hot_water": {
"description": "Gas multipoint",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"post_town": "LONDON",
"built_form": 3,
"created_at": "2019-02-11 19:55:01.000000",
"door_count": 1,
"glazed_area": 1,
"region_code": 17,
"report_type": 2,
"sap_heating": {
"cylinder_size": 1,
"water_heating_code": 908,
"water_heating_fuel": 26,
"instantaneous_wwhrs": {
"rooms_with_bath_and_or_shower": 2,
"rooms_with_mixer_shower_no_bath": 0,
"rooms_with_bath_and_mixer_shower": 0
},
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 29,
"heat_emitter_type": 0,
"emitter_temperature": "NA",
"main_heating_number": 1,
"main_heating_control": 2602,
"main_heating_category": 10,
"main_heating_fraction": 1,
"sap_main_heating_code": 691,
"main_heating_data_source": 2
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 9.93,
"schema_type": "RdSAP-Schema-18.0",
"uprn_source": "Energy Assessor",
"country_code": "EAW",
"main_heating": [
{
"description": "Room heaters, electric",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 2
}
],
"dwelling_type": "Ground-floor maisonette",
"language_code": 1,
"property_type": 3,
"address_line_1": "8, Booth Close",
"assessment_type": "RdSAP",
"completion_date": "2019-02-11",
"inspection_date": "2019-02-09",
"extensions_count": 1,
"measurement_type": 1,
"sap_flat_details": {
"level": 1,
"top_storey": "N",
"flat_location": 0,
"heat_loss_corridor": 0
},
"total_floor_area": 78,
"transaction_type": 8,
"conservatory_type": 1,
"heated_room_count": 4,
"registration_date": "2019-02-11",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 2,
"photovoltaic_supply": {
"none_or_no_details": {
"pv_connection": 0,
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"wind_turbines_terrain_type": 1
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 260,
"floor_heat_loss": 7,
"roof_construction": 3,
"wall_construction": 8,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.36,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 36.42,
"quantity": "square metres"
},
"party_wall_length": {
"value": 5.64,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 19.05,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.24,
"quantity": "metres"
},
"total_floor_area": {
"value": 36.42,
"quantity": "square metres"
},
"party_wall_length": {
"value": 5.64,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 12.95,
"quantity": "metres"
}
}
],
"wall_insulation_type": 3,
"construction_age_band": "E",
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": "ND",
"roof_insulation_thickness": "ND",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
},
{
"identifier": "Extension 1",
"wall_dry_lined": "N",
"wall_thickness": 260,
"floor_heat_loss": 1,
"roof_construction": 3,
"wall_construction": 8,
"building_part_number": 2,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.24,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 5,
"quantity": "square metres"
},
"party_wall_length": {
"value": 0.82,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 6.82,
"quantity": "metres"
}
}
],
"wall_insulation_type": 3,
"construction_age_band": "E",
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": "ND",
"roof_insulation_thickness": "ND",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
}
],
"low_energy_lighting": 100,
"solar_water_heating": "N",
"habitable_room_count": 4,
"heating_cost_current": {
"value": 944,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 3.3,
"energy_rating_average": 60,
"energy_rating_current": 50,
"lighting_cost_current": {
"value": 60,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Appliance thermostats",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"multiple_glazing_type": 2,
"open_fireplaces_count": 0,
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 297,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 92,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 122,
"currency": "GBP"
},
"indicative_cost": "6,000",
"improvement_type": "W2",
"improvement_details": {
"improvement_number": 58
},
"improvement_category": 5,
"energy_performance_rating": 55,
"environmental_impact_rating": 63
},
{
"sequence": 2,
"typical_saving": {
"value": 519,
"currency": "GBP"
},
"indicative_cost": "7,000",
"improvement_type": "T",
"improvement_details": {
"improvement_number": 29
},
"improvement_category": 5,
"energy_performance_rating": 77,
"environmental_impact_rating": 77
}
],
"co2_emissions_potential": 1.8,
"energy_rating_potential": 77,
"lighting_cost_potential": {
"value": 60,
"currency": "GBP"
},
"schema_version_original": "LIG-18.0",
"alternative_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 515,
"currency": "GBP"
},
"improvement_type": "J2",
"improvement_details": {
"improvement_number": 54
},
"improvement_category": 6,
"energy_performance_rating": 78,
"environmental_impact_rating": 96
},
{
"sequence": 2,
"typical_saving": {
"value": 413,
"currency": "GBP"
},
"improvement_type": "Z1",
"improvement_details": {
"improvement_number": 51
},
"improvement_category": 6,
"energy_performance_rating": 77,
"environmental_impact_rating": 79
},
{
"sequence": 3,
"typical_saving": {
"value": 500,
"currency": "GBP"
},
"improvement_type": "Z3",
"improvement_details": {
"improvement_number": 53
},
"improvement_category": 6,
"energy_performance_rating": 76,
"environmental_impact_rating": 73
}
],
"hot_water_cost_potential": {
"value": 97,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 1519,
"space_heating_existing_dwelling": 5050
},
"energy_consumption_current": 250,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "3.08r09",
"energy_consumption_potential": 134,
"environmental_impact_current": 58,
"fixed_lighting_outlets_count": 6,
"current_energy_efficiency_band": "E",
"environmental_impact_potential": 77,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 43,
"low_energy_fixed_lighting_outlets_count": 6
}

View file

@ -0,0 +1,510 @@
{
"uprn": 100040550095,
"roofs": [
{
"description": "Pitched, 175 mm loft insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"walls": [
{
"description": "Granite or whin, as built, no insulation (assumed)",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
},
{
"description": "Solid brick, as built, no insulation (assumed)",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
}
],
"floors": [
{
"description": "Solid, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 3,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"addendum": {
"stone_walls": "true"
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "TQ1 2BW",
"hot_water": {
"description": "Electric immersion, standard tariff",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 5
},
"post_town": "TORQUAY",
"built_form": 4,
"created_at": "2026-05-20 13:21:34",
"door_count": 2,
"region_code": 15,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 2,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 2
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 903,
"water_heating_fuel": 29,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 29,
"heat_emitter_type": 0,
"emitter_temperature": "NA",
"main_heating_number": 1,
"main_heating_control": 2603,
"main_heating_category": 10,
"main_heating_fraction": 1,
"sap_main_heating_code": 691,
"main_heating_data_source": 2
}
],
"immersion_heating_type": 2,
"cylinder_insulation_type": 2,
"has_fixed_air_conditioning": "false",
"cylinder_insulation_thickness": 38
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.91,
"window_height": 1.64,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.91,
"window_height": 1.64,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.62,
"window_height": 0.91,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.2,
"window_height": 0.88,
"draught_proofed": "true",
"window_location": 1,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.02,
"window_height": 0.82,
"draught_proofed": "true",
"window_location": 1,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.6,
"window_height": 1.14,
"draught_proofed": "true",
"window_location": 1,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Room heaters, electric",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 5
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Mid-terrace house",
"language_code": 1,
"pressure_test": 4,
"property_type": 0,
"address_line_1": "11 Meadfoot Lane",
"assessment_type": "RdSAP",
"completion_date": "2026-05-20",
"inspection_date": "2026-05-20",
"extensions_count": 1,
"measurement_type": 1,
"total_floor_area": 59,
"transaction_type": 8,
"conservatory_type": 1,
"heated_room_count": 3,
"registration_date": "2026-05-20",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 2,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "true",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "true"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"extract_fans_count": 1,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 510,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 1,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.31,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 17.29,
"quantity": "square metres"
},
"party_wall_length": {
"value": 7.27,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 4.72,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.35,
"quantity": "metres"
},
"total_floor_area": {
"value": 17.29,
"quantity": "square metres"
},
"party_wall_length": {
"value": 7.27,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 4.72,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "A",
"party_wall_construction": 1,
"wall_thickness_measured": "Y",
"roof_insulation_location": 2,
"roof_insulation_thickness": "175mm",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
},
{
"identifier": "Extension 1",
"wall_dry_lined": "N",
"wall_thickness": 185,
"floor_heat_loss": 7,
"sap_room_in_roof": {
"floor_area": 12.24,
"room_in_roof_type_2": {
"gable_wall_type_1": 1,
"gable_wall_type_2": 3,
"gable_wall_height_1": 2.5,
"gable_wall_height_2": 2.5,
"gable_wall_length_1": 7.39,
"gable_wall_length_2": 7.39,
"common_wall_height_1": 1.5,
"common_wall_height_2": 1.5,
"common_wall_length_1": 1.4,
"common_wall_length_2": 1.4
},
"construction_age_band": "A"
},
"roof_construction": 5,
"wall_construction": 3,
"building_part_number": 2,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.27,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 12.24,
"quantity": "square metres"
},
"party_wall_length": {
"value": 7.39,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 2.8,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "A",
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": 4,
"roof_insulation_thickness": "ND",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 1266,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 1.1,
"energy_rating_average": 60,
"energy_rating_current": 40,
"lighting_cost_current": {
"value": 43,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer and appliance thermostats",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 366,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 679,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 449,
"currency": "GBP"
},
"indicative_cost": "\u00a37,500 - \u00a311,000",
"improvement_type": "Q",
"improvement_details": {
"improvement_number": 7
},
"improvement_category": 5,
"energy_performance_rating": 55,
"environmental_impact_rating": 88
},
{
"sequence": 2,
"typical_saving": {
"value": 57,
"currency": "GBP"
},
"indicative_cost": "\u00a35,000 - \u00a310,000",
"improvement_type": "W2",
"improvement_details": {
"improvement_number": 58
},
"improvement_category": 5,
"energy_performance_rating": 56,
"environmental_impact_rating": 89
},
{
"sequence": 3,
"typical_saving": {
"value": 100,
"currency": "GBP"
},
"indicative_cost": "\u00a320 - \u00a340",
"improvement_type": "C",
"improvement_details": {
"improvement_number": 2
},
"improvement_category": 5,
"energy_performance_rating": 59,
"environmental_impact_rating": 89
},
{
"sequence": 4,
"typical_saving": {
"value": 671,
"currency": "GBP"
},
"indicative_cost": "\u00a33,500 - \u00a310,000",
"improvement_type": "T",
"improvement_details": {
"improvement_number": 29
},
"improvement_category": 5,
"energy_performance_rating": 76,
"environmental_impact_rating": 83
},
{
"sequence": 5,
"typical_saving": {
"value": 220,
"currency": "GBP"
},
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 81,
"environmental_impact_rating": 84
}
],
"co2_emissions_potential": 1.1,
"energy_rating_potential": 81,
"lighting_cost_potential": {
"value": 43,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"alternative_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 822,
"currency": "GBP"
},
"improvement_type": "J2",
"improvement_details": {
"improvement_number": 54
},
"improvement_category": 6,
"energy_performance_rating": 79,
"environmental_impact_rating": 96
},
{
"sequence": 2,
"typical_saving": {
"value": 524,
"currency": "GBP"
},
"improvement_type": "Z3",
"improvement_details": {
"improvement_number": 53
},
"improvement_category": 6,
"energy_performance_rating": 71,
"environmental_impact_rating": 74
}
],
"hot_water_cost_potential": {
"value": 301,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2452.35,
"space_heating_existing_dwelling": 4576.09
},
"draughtproofed_door_count": 2,
"energy_consumption_current": 189,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 102,
"environmental_impact_current": 84,
"current_energy_efficiency_band": "E",
"environmental_impact_potential": 84,
"led_fixed_lighting_bulbs_count": 7,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 18,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,700 @@
{
"uprn": 100050355518,
"roofs": [
{
"description": "Pitched, 200 mm loft insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
{
"description": "Pitched, limited insulation",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
}
],
"walls": [
{
"description": "Cavity wall, as built, no insulation (assumed)",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
}
],
"floors": [
{
"description": "Suspended, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 1,
"window": {
"description": "Partial double glazing",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
},
"addendum": {
"addendum_numbers": [
15
],
"cavity_fill_recommended": "true"
},
"lighting": {
"description": "Below average lighting efficiency",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"postcode": "BD23 1BG",
"hot_water": {
"description": "From main system, no cylinder thermostat",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"post_town": "SKIPTON",
"psv_count": 0,
"built_form": 2,
"created_at": "2026-05-18 08:00:25",
"door_count": 1,
"region_code": 7,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 2,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 2
},
{
"shower_wwhrs": 1,
"shower_outlet_type": 2
}
],
"water_heating_code": 901,
"water_heating_fuel": 26,
"cylinder_thermostat": "N",
"secondary_fuel_type": 5,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "N",
"heat_emitter_type": 1,
"main_heating_number": 1,
"main_heating_control": 2102,
"main_heating_category": 2,
"main_heating_fraction": 1,
"sap_main_heating_code": 117,
"central_heating_pump_age": 0,
"main_heating_data_source": 2
}
],
"immersion_heating_type": "NA",
"secondary_heating_type": 631,
"cylinder_insulation_type": 2,
"has_fixed_air_conditioning": "false",
"cylinder_insulation_thickness": 80
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 1,
"window_type": 1,
"glazing_type": 5,
"window_width": 0.4,
"window_height": 1.18,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 1,
"window_type": 1,
"glazing_type": 5,
"window_width": 5.03,
"window_height": 1.72,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.19,
"window_height": 1,
"draught_proofed": "false",
"window_location": 2,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.74,
"window_height": 2.05,
"draught_proofed": "false",
"window_location": 2,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 2,
"glazing_type": 3,
"window_width": 0.6,
"window_height": 0.9,
"draught_proofed": "false",
"window_location": 2,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 2,
"glazing_type": 3,
"window_width": 0.6,
"window_height": 0.9,
"draught_proofed": "false",
"window_location": 2,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": 2.34,
"window_height": 1.18,
"draught_proofed": "false",
"window_location": 1,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.79,
"window_height": 2.06,
"draught_proofed": "false",
"window_location": 2,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 5,
"window_width": 5.03,
"window_height": 1.23,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 5,
"window_width": 1.11,
"window_height": 1.17,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.19,
"window_height": 1,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.18,
"window_height": 1.15,
"draught_proofed": "false",
"window_location": 1,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.1,
"window_height": 1,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.66,
"window_height": 1.17,
"draught_proofed": "false",
"window_location": 1,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Boiler and radiators, mains gas",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Semi-detached house",
"language_code": 1,
"pressure_test": 4,
"property_type": 0,
"address_line_1": "26 Regent Crescent",
"assessment_type": "RdSAP",
"completion_date": "2026-05-18",
"inspection_date": "2026-05-15",
"extensions_count": 2,
"measurement_type": 1,
"open_flues_count": 0,
"total_floor_area": 122,
"transaction_type": 15,
"conservatory_type": 1,
"has_draught_lobby": "true",
"heated_room_count": 5,
"other_flues_count": 0,
"registration_date": "2026-05-18",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 2,
"pv_diverter": "false",
"pv_connection": 0,
"pv_battery_count": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false",
"is_hydro_output_connected_to_dwelling_meter": "false"
},
"secondary_heating": {
"description": "Room heaters, anthracite",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"closed_flues_count": 0,
"extract_fans_count": 1,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 230,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.53,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 35.63,
"quantity": "square metres"
},
"party_wall_length": {
"value": 6.09,
"quantity": "metres"
},
"floor_construction": 2,
"heat_loss_perimeter": {
"value": 7.95,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.38,
"quantity": "metres"
},
"total_floor_area": {
"value": 35.63,
"quantity": "square metres"
},
"party_wall_length": {
"value": 6.09,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 13.37,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "C",
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": 2,
"roof_insulation_thickness": "200mm",
"wall_insulation_thickness": "NI"
},
{
"identifier": "Extension 1",
"wall_dry_lined": "N",
"wall_thickness": 320,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 4,
"building_part_number": 2,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.29,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 15.62,
"quantity": "square metres"
},
"party_wall_length": 0,
"floor_construction": 2,
"heat_loss_perimeter": {
"value": 7.95,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.24,
"quantity": "metres"
},
"total_floor_area": {
"value": 15.62,
"quantity": "square metres"
},
"party_wall_length": 0,
"heat_loss_perimeter": {
"value": 11.19,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "C",
"party_wall_construction": "NA",
"wall_thickness_measured": "Y",
"roof_insulation_location": 2,
"roof_insulation_thickness": "200mm",
"wall_insulation_thickness": "NI"
},
{
"identifier": "Extension 2",
"wall_dry_lined": "N",
"wall_thickness": 320,
"floor_heat_loss": 7,
"roof_construction": 8,
"wall_construction": 4,
"building_part_number": 3,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.86,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 19.28,
"quantity": "square metres"
},
"party_wall_length": 0,
"floor_construction": 2,
"heat_loss_perimeter": {
"value": 12.66,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "C",
"party_wall_construction": "NA",
"wall_thickness_measured": "Y",
"roof_insulation_location": 7,
"wall_insulation_thickness": "NI",
"sloping_ceiling_insulation_thickness": "AB"
}
],
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"solar_water_heating": "N",
"habitable_room_count": 5,
"heating_cost_current": {
"value": 3523,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 12,
"energy_rating_average": 60,
"energy_rating_current": 36,
"lighting_cost_current": {
"value": 84,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, no room thermostat",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
}
],
"blocked_chimneys_count": 1,
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 1387,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 574,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 0,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 260,
"indicative_cost": "\u00a3900 - \u00a31,200",
"improvement_type": "A2",
"improvement_details": {
"improvement_number": 45
},
"improvement_category": 5,
"energy_performance_rating": 39,
"environmental_impact_rating": 30
},
{
"sequence": 2,
"typical_saving": 606,
"indicative_cost": "\u00a3900 - \u00a31,500",
"improvement_type": "B",
"improvement_details": {
"improvement_number": 6
},
"improvement_category": 5,
"energy_performance_rating": 47,
"environmental_impact_rating": 38
},
{
"sequence": 3,
"typical_saving": 251,
"indicative_cost": "\u00a35,000 - \u00a310,000",
"improvement_type": "W1",
"improvement_details": {
"improvement_number": 57
},
"improvement_category": 5,
"energy_performance_rating": 51,
"environmental_impact_rating": 42
},
{
"sequence": 4,
"typical_saving": 134,
"indicative_cost": "\u00a3150 - \u00a3250",
"improvement_type": "D",
"improvement_details": {
"improvement_number": 10
},
"improvement_category": 5,
"energy_performance_rating": 53,
"environmental_impact_rating": 44
},
{
"sequence": 5,
"typical_saving": 276,
"indicative_cost": "\u00a3220 - \u00a3250",
"improvement_type": "G",
"improvement_details": {
"improvement_number": 12
},
"improvement_category": 5,
"energy_performance_rating": 57,
"environmental_impact_rating": 49
},
{
"sequence": 6,
"typical_saving": 629,
"indicative_cost": "\u00a32,200 - \u00a33,500",
"improvement_type": "I",
"improvement_details": {
"improvement_number": 20
},
"improvement_category": 5,
"energy_performance_rating": 67,
"environmental_impact_rating": 61
},
{
"sequence": 7,
"typical_saving": 197,
"indicative_cost": "\u00a34,500 - \u00a36,000",
"improvement_type": "O",
"improvement_details": {
"improvement_number": 8
},
"improvement_category": 5,
"energy_performance_rating": 70,
"environmental_impact_rating": 66
},
{
"sequence": 8,
"typical_saving": 233,
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 74,
"environmental_impact_rating": 67
}
],
"co2_emissions_potential": 4.6,
"energy_rating_potential": 74,
"lighting_cost_potential": {
"value": 84,
"currency": "GBP"
},
"schema_version_original": "LIG-21.0.1",
"alternative_improvements": [
{
"sequence": 1,
"typical_saving": 172,
"improvement_type": "Q2",
"improvement_details": {
"improvement_number": 55
},
"improvement_category": 6,
"energy_performance_rating": 50,
"environmental_impact_rating": 40
},
{
"sequence": 2,
"typical_saving": 774,
"improvement_type": "J2",
"improvement_details": {
"improvement_number": 54
},
"improvement_category": 6,
"energy_performance_rating": 64,
"environmental_impact_rating": 82
},
{
"sequence": 3,
"typical_saving": 828,
"improvement_type": "Z3",
"improvement_details": {
"improvement_number": 53
},
"improvement_category": 6,
"energy_performance_rating": 69,
"environmental_impact_rating": 66
}
],
"flueless_gas_fires_count": 0,
"hot_water_cost_potential": {
"value": 358,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 3736.92,
"space_heating_existing_dwelling": 24523.93
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 466,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 52,
"calculation_software_version": "4.3.74",
"energy_consumption_potential": 170,
"environmental_impact_current": 27,
"current_energy_efficiency_band": "F",
"environmental_impact_potential": 67,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 98,
"low_energy_fixed_lighting_bulbs_count": 17,
"incandescent_fixed_lighting_bulbs_count": 4
}

View file

@ -0,0 +1,426 @@
{
"uprn": 100050881708,
"roofs": [
{
"description": "Pitched, 225 mm loft insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"walls": [
{
"description": "Cavity wall, filled cavity",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "To unheated space, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 3,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"lighting": {
"description": "Below average lighting efficiency",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"postcode": "S65 3RW",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "ROTHERHAM",
"built_form": 2,
"created_at": "2026-05-27 15:09:39",
"door_count": 2,
"region_code": 3,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 1,
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 26,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "Y",
"heat_emitter_type": 1,
"emitter_temperature": 0,
"main_heating_number": 1,
"main_heating_control": 2106,
"main_heating_category": 2,
"main_heating_fraction": 1,
"central_heating_pump_age": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 17741
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 7,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.7,
"window_height": 1.4,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 7,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.2,
"window_height": 1,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 7,
"window_type": 1,
"glazing_type": 2,
"window_width": 0.6,
"window_height": 1,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 1,
"glazing_type": 2,
"window_width": 0.6,
"window_height": 0.7,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 1,
"glazing_type": 2,
"window_width": 0.6,
"window_height": 1,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.2,
"window_height": 1,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.2,
"window_height": 1,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.1,
"window_height": 0.8,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 1,
"window_type": 1,
"glazing_type": 2,
"window_width": 0.6,
"window_height": 0.7,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 1,
"window_type": 1,
"glazing_type": 2,
"window_width": 0.6,
"window_height": 1.1,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Boiler and radiators, mains gas",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Semi-detached house",
"language_code": 1,
"pressure_test": 4,
"property_type": 0,
"address_line_1": "9 Woodgrove Road",
"assessment_type": "RdSAP",
"completion_date": "2026-05-27",
"inspection_date": "2026-05-27",
"extensions_count": 0,
"measurement_type": 1,
"total_floor_area": 63,
"transaction_type": 14,
"conservatory_type": 1,
"heated_room_count": 3,
"registration_date": "2026-05-27",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 2,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "true",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "true"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"extract_fans_count": 2,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"floor_heat_loss": 2,
"roof_construction": 4,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.4,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 31.62,
"quantity": "square metres"
},
"party_wall_length": {
"value": 6.2,
"quantity": "metres"
},
"floor_construction": 2,
"heat_loss_perimeter": {
"value": 16.4,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.41,
"quantity": "metres"
},
"total_floor_area": {
"value": 31.62,
"quantity": "square metres"
},
"party_wall_length": {
"value": 6.2,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 16.4,
"quantity": "metres"
}
}
],
"wall_insulation_type": 2,
"construction_age_band": "B",
"party_wall_construction": 1,
"wall_thickness_measured": "N",
"roof_insulation_location": 2,
"roof_insulation_thickness": "225mm",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 777,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 2.3,
"energy_rating_average": 60,
"energy_rating_current": 70,
"lighting_cost_current": {
"value": 79,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, room thermostat and TRVs",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 671,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 172,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 110,
"currency": "GBP"
},
"indicative_cost": "\u00a35,000 - \u00a310,000",
"improvement_type": "W1",
"improvement_details": {
"improvement_number": 57
},
"improvement_category": 5,
"energy_performance_rating": 73,
"environmental_impact_rating": 75
},
{
"sequence": 2,
"typical_saving": {
"value": 30,
"currency": "GBP"
},
"indicative_cost": "\u00a3120 - \u00a3140",
"improvement_type": "E",
"improvement_details": {
"improvement_number": 35
},
"improvement_category": 5,
"energy_performance_rating": 74,
"environmental_impact_rating": 75
},
{
"sequence": 3,
"typical_saving": {
"value": 184,
"currency": "GBP"
},
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 79,
"environmental_impact_rating": 77
}
],
"co2_emissions_potential": 1.9,
"energy_rating_potential": 79,
"lighting_cost_potential": {
"value": 45,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 172,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2070.91,
"space_heating_existing_dwelling": 7543.77
},
"draughtproofed_door_count": 2,
"energy_consumption_current": 205,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 161,
"environmental_impact_current": 71,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 77,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 37,
"low_energy_fixed_lighting_bulbs_count": 4,
"incandescent_fixed_lighting_bulbs_count": 4
}

View file

@ -0,0 +1,351 @@
{
"uprn": 100051051866,
"roofs": [
{
"description": "Pitched, insulated (assumed)",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"walls": [
{
"description": "Cavity wall, as built, insulated (assumed)",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "Suspended, limited insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 1,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"addendum": {
"addendum_numbers": [
15
]
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "S20 7NQ",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "SHEFFIELD",
"psv_count": 0,
"built_form": 1,
"created_at": "2026-05-18 17:37:45",
"door_count": 1,
"region_code": 11,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 2,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 4
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 26,
"cylinder_thermostat": "Y",
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "Y",
"heat_emitter_type": 1,
"emitter_temperature": 1,
"main_heating_number": 1,
"main_heating_control": 2104,
"main_heating_category": 2,
"main_heating_fraction": 1,
"sap_main_heating_code": 102,
"central_heating_pump_age": 0,
"main_heating_data_source": 2,
"main_heating_index_number": 18773
}
],
"immersion_heating_type": "NA",
"cylinder_insulation_type": 1,
"has_fixed_air_conditioning": "false",
"cylinder_insulation_thickness": 50
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 7,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1.6,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 7,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1.2,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 7,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1.6,
"quantity": "m"
},
"window_height": {
"value": 1.6,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1.6,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 0.5,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Boiler and radiators, mains gas",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Detached bungalow",
"language_code": 1,
"pressure_test": 4,
"property_type": 1,
"address_line_1": "3 Pentland Gardens",
"address_line_2": "Waterthorpe",
"assessment_type": "RdSAP",
"completion_date": "2026-05-18",
"inspection_date": "2026-05-18",
"extensions_count": 0,
"measurement_type": 1,
"open_flues_count": 0,
"total_floor_area": 60,
"transaction_type": 1,
"conservatory_type": 2,
"heated_room_count": 4,
"other_flues_count": 0,
"registration_date": "2026-05-18",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 3,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"closed_flues_count": 0,
"extract_fans_count": 0,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 300,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": 2.4,
"floor_insulation": 1,
"total_floor_area": 60,
"party_wall_length": 0,
"floor_construction": 2,
"heat_loss_perimeter": 32
}
],
"wall_insulation_type": 4,
"construction_age_band": "I",
"party_wall_construction": "NA",
"wall_thickness_measured": "Y",
"roof_insulation_location": 4,
"roof_insulation_thickness": "NI"
}
],
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"solar_water_heating": "N",
"habitable_room_count": 4,
"heating_cost_current": {
"value": 701,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 2.3,
"energy_rating_average": 60,
"energy_rating_current": 70,
"lighting_cost_current": {
"value": 40,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer and room thermostat",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"blocked_chimneys_count": 0,
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 701,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 235,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"schema_version_current": "LIG-21.0",
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 238,
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 82,
"environmental_impact_rating": 73
}
],
"co2_emissions_potential": 2.1,
"energy_rating_potential": 82,
"lighting_cost_potential": {
"value": 40,
"currency": "GBP"
},
"schema_version_original": "LIG-21.0",
"hot_water_cost_potential": {
"value": 235,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2544.67,
"space_heating_existing_dwelling": 6292.1
},
"draughtproofed_door_count": 1,
"energy_consumption_current": 209,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "10.2.2.0",
"energy_consumption_potential": 185,
"environmental_impact_current": 70,
"cfl_fixed_lighting_bulbs_count": 0,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 73,
"led_fixed_lighting_bulbs_count": 8,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 38,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,410 @@
{
"uprn": 100061275133,
"roofs": [
{
"description": "Pitched, 300 mm loft insulation",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
}
],
"walls": [
{
"description": "Solid brick, with external insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "Suspended, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 2,
"window": {
"description": "High performance glazing",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "ME7 2ES",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "GILLINGHAM",
"built_form": 2,
"created_at": "2026-05-20 08:39:48",
"door_count": 2,
"region_code": 14,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 1,
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 26,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "Y",
"heat_emitter_type": 1,
"emitter_temperature": 0,
"main_heating_number": 1,
"main_heating_control": 2106,
"main_heating_category": 2,
"main_heating_fraction": 1,
"central_heating_pump_age": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 18908
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"orientation": 8,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.19,
"window_height": 1.13,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 8,
"window_type": 1,
"glazing_type": 13,
"window_width": 0.57,
"window_height": 0.55,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 8,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.19,
"window_height": 1.11,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 2,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.05,
"window_height": 0.85,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 2,
"window_type": 1,
"glazing_type": 13,
"window_width": 0.57,
"window_height": 0.56,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 4,
"window_type": 1,
"glazing_type": 13,
"window_width": 2.11,
"window_height": 1.13,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 4,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.8,
"window_height": 0.97,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 4,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.8,
"window_height": 0.97,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 4,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.2,
"window_height": 0.99,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 8,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.8,
"window_height": 0.97,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 8,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.19,
"window_height": 0.97,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 8,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.18,
"window_height": 0.99,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Boiler and radiators, mains gas",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Semi-detached house",
"language_code": 1,
"pressure_test": 4,
"property_type": 0,
"address_line_1": "75 Toronto Road",
"assessment_type": "RdSAP",
"completion_date": "2026-05-20",
"inspection_date": "2026-05-19",
"extensions_count": 0,
"measurement_type": 1,
"total_floor_area": 78,
"transaction_type": 8,
"conservatory_type": 1,
"heated_room_count": 3,
"registration_date": "2026-05-20",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 2,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "true",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "true"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 400,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 3,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.4,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 38.99,
"quantity": "square metres"
},
"party_wall_length": {
"value": 5.09,
"quantity": "metres"
},
"floor_construction": 0,
"heat_loss_perimeter": {
"value": 17.92,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.4,
"quantity": "metres"
},
"total_floor_area": {
"value": 38.99,
"quantity": "square metres"
},
"party_wall_length": {
"value": 5.09,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 17.92,
"quantity": "metres"
}
}
],
"wall_insulation_type": 1,
"construction_age_band": "B",
"party_wall_construction": 1,
"wall_thickness_measured": "Y",
"roof_insulation_location": 2,
"roof_insulation_thickness": "300mm",
"wall_insulation_thickness": "100mm",
"floor_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 608,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 1.9,
"energy_rating_average": 60,
"energy_rating_current": 76,
"lighting_cost_current": {
"value": 58,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, room thermostat and TRVs",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 608,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 182,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 216,
"currency": "GBP"
},
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 81,
"environmental_impact_rating": 79
}
],
"co2_emissions_potential": 1.8,
"energy_rating_potential": 81,
"lighting_cost_potential": {
"value": 58,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 182,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2203.16,
"space_heating_existing_dwelling": 5517.85
},
"draughtproofed_door_count": 2,
"energy_consumption_current": 133,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 118,
"environmental_impact_current": 78,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 79,
"led_fixed_lighting_bulbs_count": 5,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 24,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,337 @@
{
"uprn": 10012334488,
"roofs": [
{
"description": "(another dwelling above)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"walls": [
{
"description": "System built, as built, insulated (assumed)",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "(another dwelling below)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 3,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "SL4 3BP",
"hot_water": {
"description": "From main system, no cylinder thermostat",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 4
},
"post_town": "WINDSOR",
"built_form": "NR",
"created_at": "2026-05-14 18:33:57",
"door_count": 1,
"region_code": 17,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 0,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 1
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 29,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 29,
"heat_emitter_type": 2,
"emitter_temperature": "NA",
"main_heating_number": 1,
"main_heating_control": 2110,
"main_heating_category": 2,
"main_heating_fraction": 1,
"sap_main_heating_code": 195,
"central_heating_pump_age": 0,
"main_heating_data_source": 2
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 2,
"glazing_type": 2,
"window_width": 0.85,
"window_height": 1.11,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 2,
"glazing_type": 2,
"window_width": 0.85,
"window_height": 1.11,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 2,
"glazing_type": 2,
"window_width": 0.85,
"window_height": 1.11,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 2,
"glazing_type": 2,
"window_width": 0.85,
"window_height": 1.11,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 2,
"glazing_type": 2,
"window_width": 0.85,
"window_height": 1.11,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 2,
"glazing_type": 2,
"window_width": 0.85,
"window_height": 1.11,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Boiler and underfloor heating, electric",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 5
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Mid-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "Flat 12",
"address_line_2": "Alexandra Court 25a",
"address_line_3": "St. Leonards Road",
"assessment_type": "RdSAP",
"completion_date": "2026-05-14",
"inspection_date": "2026-05-14",
"extensions_count": 0,
"measurement_type": 1,
"sap_flat_details": {
"level": 2,
"top_storey": "N",
"storey_count": 4,
"flat_location": 3,
"heat_loss_corridor": 1
},
"total_floor_area": 62,
"transaction_type": 14,
"conservatory_type": 1,
"has_draught_lobby": "true",
"heated_room_count": 2,
"registration_date": "2026-05-14",
"sap_energy_source": {
"mains_gas": "N",
"meter_type": 1,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 1,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"extract_fans_count": 2,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 260,
"floor_heat_loss": 6,
"roof_construction": 3,
"wall_construction": 8,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.48,
"quantity": "metres"
},
"total_floor_area": {
"value": 62,
"quantity": "square metres"
},
"party_wall_length": {
"value": 23.99,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 11.89,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "L",
"party_wall_construction": 1,
"wall_thickness_measured": "Y",
"roof_insulation_location": "ND",
"roof_insulation_thickness": "ND",
"wall_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 2,
"heating_cost_current": {
"value": 136,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 0.5,
"energy_rating_average": 60,
"energy_rating_current": 72,
"lighting_cost_current": {
"value": 51,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Time and temperature zone control",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
}
],
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 136,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 893,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 91,
"currency": "GBP"
},
"indicative_cost": "\u00a3600 - \u00a31,500",
"improvement_type": "Y",
"improvement_details": {
"improvement_number": 49
},
"improvement_category": 5,
"energy_performance_rating": 74,
"environmental_impact_rating": 93
}
],
"co2_emissions_potential": 0.5,
"energy_rating_potential": 74,
"lighting_cost_potential": {
"value": 51,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 802,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2802.07,
"space_heating_existing_dwelling": 628.72
},
"draughtproofed_door_count": 1,
"energy_consumption_current": 92,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 85,
"environmental_impact_current": 93,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 93,
"led_fixed_lighting_bulbs_count": 23,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 9,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,491 @@
{
"uprn": 10070056075,
"roofs": [
{
"description": "Pitched, insulated",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"walls": [
{
"description": "Cavity wall, as built, insulated (assumed)",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "Suspended, insulated (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 1,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"addendum": {
"addendum_numbers": [
15
]
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "BD10 9UG",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "BRADFORD",
"psv_count": 0,
"built_form": 1,
"created_at": "2026-05-15 10:22:34",
"door_count": 1,
"region_code": 11,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 4,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 4
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 26,
"cylinder_thermostat": "Y",
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "Y",
"heat_emitter_type": 1,
"emitter_temperature": 1,
"main_heating_number": 1,
"main_heating_control": 2106,
"main_heating_category": 2,
"main_heating_fraction": 1,
"central_heating_pump_age": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 19012
}
],
"immersion_heating_type": "NA",
"cylinder_insulation_type": 1,
"has_fixed_air_conditioning": "false",
"cylinder_insulation_thickness": 80
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Boiler and radiators, mains gas",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Detached house",
"language_code": 1,
"pressure_test": 4,
"property_type": 0,
"address_line_1": "36 Longlands",
"address_line_2": "Idle",
"assessment_type": "RdSAP",
"completion_date": "2026-05-15",
"inspection_date": "2026-05-08",
"extensions_count": 0,
"measurement_type": 1,
"open_flues_count": 0,
"total_floor_area": 139,
"transaction_type": 5,
"conservatory_type": 1,
"heated_room_count": 8,
"other_flues_count": 0,
"registration_date": "2026-05-15",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 1,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"closed_flues_count": 0,
"extract_fans_count": 0,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 400,
"floor_heat_loss": 7,
"roof_construction": 8,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": 2.3,
"floor_insulation": 1,
"total_floor_area": 69.6,
"party_wall_length": 0,
"floor_construction": 2,
"heat_loss_perimeter": 37.7
},
{
"floor": 1,
"room_height": 2.3,
"total_floor_area": 69.6,
"party_wall_length": 0,
"heat_loss_perimeter": 37.7
}
],
"wall_insulation_type": 4,
"construction_age_band": "K",
"party_wall_construction": "NA",
"wall_thickness_measured": "N",
"roof_insulation_location": 7,
"wall_insulation_thickness": "NI",
"sloping_ceiling_insulation_thickness": "AB"
}
],
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"solar_water_heating": "N",
"habitable_room_count": 8,
"heating_cost_current": {
"value": 938,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 3.0,
"energy_rating_average": 60,
"energy_rating_current": 80,
"lighting_cost_current": {
"value": 89,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, room thermostat and TRVs",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"blocked_chimneys_count": 0,
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 938,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 236,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"schema_version_current": "LIG-21.0",
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 308,
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 87,
"environmental_impact_rating": 81
}
],
"co2_emissions_potential": 2.8,
"energy_rating_potential": 87,
"lighting_cost_potential": {
"value": 89,
"currency": "GBP"
},
"schema_version_original": "LIG-21.0",
"hot_water_cost_potential": {
"value": 236,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2744.25,
"space_heating_existing_dwelling": 9473.16
},
"draughtproofed_door_count": 1,
"energy_consumption_current": 117,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "10.2.2.0",
"energy_consumption_potential": 106,
"environmental_impact_current": 79,
"cfl_fixed_lighting_bulbs_count": 0,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 81,
"led_fixed_lighting_bulbs_count": 18,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 21,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,316 @@
{
"uprn": 10091578598,
"roofs": [
{
"description": "(another dwelling above)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"walls": [
{
"description": "Cavity wall, as built, insulated (assumed)",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "(another dwelling below)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 2,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"lighting": {
"description": "Good lighting efficiency",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"postcode": "RM1 3FB",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 5
},
"post_town": "ROMFORD",
"built_form": "NR",
"created_at": "2026-05-16 17:47:19",
"door_count": 1,
"region_code": 2,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 4,
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 29,
"cylinder_thermostat": "Y",
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 29,
"heat_emitter_type": 1,
"emitter_temperature": "NA",
"main_heating_number": 1,
"main_heating_control": 2106,
"main_heating_category": 2,
"main_heating_fraction": 1,
"sap_main_heating_code": 195,
"central_heating_pump_age": 0,
"main_heating_data_source": 2
}
],
"immersion_heating_type": "NA",
"cylinder_insulation_type": 1,
"has_fixed_air_conditioning": "false",
"cylinder_insulation_thickness": 50
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 2,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.15,
"window_height": 1.65,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 2,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.15,
"window_height": 1.65,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 2,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.15,
"window_height": 1.65,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 2,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.15,
"window_height": 1.65,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Boiler and radiators, electric",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 5
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Mid-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "41 Verve Apartments",
"address_line_2": "5 Mercury Gardens",
"assessment_type": "RdSAP",
"completion_date": "2026-05-16",
"inspection_date": "2026-05-16",
"extensions_count": 0,
"measurement_type": 1,
"sap_flat_details": {
"level": 2,
"top_storey": "N",
"storey_count": 6,
"flat_location": 3,
"heat_loss_corridor": 2,
"unheated_corridor_length": 6
},
"total_floor_area": 60,
"transaction_type": 8,
"conservatory_type": 1,
"heated_room_count": 2,
"registration_date": "2026-05-16",
"sap_energy_source": {
"mains_gas": "N",
"meter_type": 4,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "true"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 300,
"floor_heat_loss": 6,
"roof_construction": 3,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.5,
"quantity": "metres"
},
"total_floor_area": {
"value": 60,
"quantity": "square metres"
},
"party_wall_length": {
"value": 20,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 12,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "I",
"sap_alternative_wall_1": {
"wall_area": 15,
"sheltered_wall": "Y",
"wall_dry_lined": "N",
"wall_thickness": 300,
"wall_construction": 4,
"wall_insulation_type": 4,
"wall_thickness_measured": "Y",
"wall_insulation_thickness": "NI"
},
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": "ND",
"roof_insulation_thickness": "ND",
"wall_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 2,
"heating_cost_current": {
"value": 202,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 0.6,
"energy_rating_average": 60,
"energy_rating_current": 71,
"lighting_cost_current": {
"value": 55,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, room thermostat and TRVs",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 370,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 847,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 80,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 76,
"currency": "GBP"
},
"indicative_cost": "\u00a3150 - \u00a3250",
"improvement_type": "D",
"improvement_details": {
"improvement_number": 10
},
"improvement_category": 5,
"energy_performance_rating": 79,
"environmental_impact_rating": 92
}
],
"co2_emissions_potential": 0.6,
"energy_rating_potential": 79,
"lighting_cost_potential": {
"value": 48,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 611,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2659.46,
"space_heating_existing_dwelling": 1061.64
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 102,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 102,
"environmental_impact_current": 92,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 92,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 10,
"low_energy_fixed_lighting_bulbs_count": 20,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,366 @@
{
"uprn": 10094975827,
"roofs": [
{
"description": "Pitched, insulated (assumed)",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"walls": [
{
"description": "Solid brick, as built, no insulation (assumed)",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
}
],
"floors": [
{
"description": "(other premises below)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 3,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "B8 2NG",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "BIRMINGHAM",
"psv_count": 0,
"built_form": 3,
"created_at": "2026-05-14 13:27:59",
"door_count": 1,
"region_code": 6,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 1,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 1
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 26,
"cylinder_thermostat": "N",
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "Y",
"heat_emitter_type": 1,
"emitter_temperature": 1,
"main_heating_number": 1,
"main_heating_control": 2107,
"main_heating_category": 2,
"main_heating_fraction": 1,
"central_heating_pump_age": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 18559
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.01,
"quantity": "m"
},
"window_height": {
"value": 1.61,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.66,
"quantity": "m"
},
"window_height": {
"value": 1.46,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 3,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.09,
"quantity": "m"
},
"window_height": {
"value": 1.29,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 3,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.09,
"quantity": "m"
},
"window_height": {
"value": 1.55,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 3,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.79,
"quantity": "m"
},
"window_height": {
"value": 1.92,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Boiler and radiators, mains gas",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Top-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "Flat 2",
"address_line_2": "874 Washwood Heath Road",
"assessment_type": "RdSAP",
"completion_date": "2026-05-14",
"inspection_date": "2026-05-13",
"extensions_count": 0,
"measurement_type": 1,
"open_flues_count": 0,
"sap_flat_details": {
"level": 3,
"top_storey": "N",
"storey_count": 2,
"flat_location": 1,
"heat_loss_corridor": 0
},
"total_floor_area": 78,
"transaction_type": 8,
"conservatory_type": 1,
"heated_room_count": 3,
"other_flues_count": 0,
"registration_date": "2026-05-14",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 3,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"closed_flues_count": 0,
"extract_fans_count": 0,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 340,
"floor_heat_loss": 3,
"roof_construction": 4,
"wall_construction": 3,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": 2.44,
"total_floor_area": 77.87,
"party_wall_length": 11.57,
"heat_loss_perimeter": 25.03
}
],
"wall_insulation_type": 4,
"construction_age_band": "B",
"party_wall_construction": 1,
"wall_thickness_measured": "Y",
"roof_insulation_location": 4,
"roof_insulation_thickness": "NI",
"wall_insulation_thickness": "NI"
}
],
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 952,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 3.2,
"energy_rating_average": 60,
"energy_rating_current": 66,
"lighting_cost_current": {
"value": 56,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, TRVs and bypass",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"blocked_chimneys_count": 0,
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 735,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 197,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"schema_version_current": "LIG-21.0",
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 171,
"indicative_cost": "\u00a37,500 - \u00a311,000",
"improvement_type": "Q",
"improvement_details": {
"improvement_number": 7
},
"improvement_category": 5,
"energy_performance_rating": 70,
"environmental_impact_rating": 70
},
{
"sequence": 2,
"typical_saving": 45,
"indicative_cost": "\u00a3220 - \u00a3250",
"improvement_type": "G",
"improvement_details": {
"improvement_number": 14
},
"improvement_category": 5,
"energy_performance_rating": 72,
"environmental_impact_rating": 71
}
],
"co2_emissions_potential": 2.6,
"energy_rating_potential": 72,
"lighting_cost_potential": {
"value": 56,
"currency": "GBP"
},
"schema_version_original": "LIG-21.0",
"hot_water_cost_potential": {
"value": 198,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2548.95,
"space_heating_existing_dwelling": 10093.12
},
"draughtproofed_door_count": 1,
"energy_consumption_current": 227,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "10.2.2.0",
"energy_consumption_potential": 181,
"environmental_impact_current": 64,
"cfl_fixed_lighting_bulbs_count": 0,
"current_energy_efficiency_band": "D",
"environmental_impact_potential": 71,
"led_fixed_lighting_bulbs_count": 8,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 41,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,321 @@
{
"uprn": 103001004,
"roofs": [
{
"description": "Pitched, Unknown loft insulation",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"walls": [
{
"description": "Cavity wall, filled cavity",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "Solid, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 2,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"lighting": {
"description": "Good lighting efficiency",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"postcode": "NE33 1TN",
"hot_water": {
"description": "Electric immersion, off-peak",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 4
},
"post_town": "SOUTH SHIELDS",
"built_form": "NR",
"created_at": "2026-04-16 10:25:37",
"door_count": 0,
"region_code": 1,
"report_type": 2,
"sap_heating": {
"number_baths": 0,
"cylinder_size": 3,
"shower_outlets": [
{
"shower_outlet": {
"shower_wwhrs": 1,
"shower_outlet_type": 1
}
}
],
"cylinder_heat_loss": 1.8,
"number_baths_wwhrs": 0,
"water_heating_code": 903,
"water_heating_fuel": 29,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 20,
"heat_emitter_type": 0,
"emitter_temperature": "NA",
"main_heating_number": 1,
"main_heating_control": 2307,
"main_heating_category": 6,
"main_heating_fraction": 1,
"sap_main_heating_code": 301,
"main_heating_data_source": 2,
"community_heat_distribution_type": 6
}
],
"immersion_heating_type": 2,
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"glazing_gap": 12,
"orientation": 7,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.21,
"window_height": 1.44,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": 12,
"orientation": 7,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.77,
"window_height": 1.14,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Community scheme",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Ground-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "3 Julius Court",
"address_line_2": "Mile End Road",
"assessment_type": "RdSAP",
"completion_date": "2026-04-16",
"inspection_date": "2026-04-16",
"extensions_count": 0,
"measurement_type": 1,
"sap_flat_details": {
"level": 1,
"top_storey": "N",
"storey_count": 1,
"flat_location": 0,
"heat_loss_corridor": 1
},
"total_floor_area": 31,
"transaction_type": 8,
"conservatory_type": 1,
"has_draught_lobby": "true",
"heated_room_count": 2,
"registration_date": "2026-04-16",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 1,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"extract_fans_count": 2,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.37,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 30.55,
"quantity": "square metres"
},
"party_wall_length": {
"value": 16.75,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 5.37,
"quantity": "metres"
}
}
],
"wall_insulation_type": 2,
"construction_age_band": "F",
"party_wall_construction": 0,
"wall_thickness_measured": "N",
"roof_insulation_location": 2,
"roof_insulation_thickness": "NI",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 2,
"heating_cost_current": {
"value": 251,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 1.2,
"energy_rating_average": 60,
"energy_rating_current": 50,
"lighting_cost_current": {
"value": 29,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Flat rate charging, TRVs",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 313,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 1054,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 35,
"currency": "GBP"
},
"indicative_cost": "\u00a3900 - \u00a31,200",
"improvement_type": "A",
"improvement_details": {
"improvement_number": 5
},
"improvement_category": 5,
"energy_performance_rating": 52,
"environmental_impact_rating": 81
},
{
"sequence": 2,
"typical_saving": {
"value": 598,
"currency": "GBP"
},
"indicative_cost": "\u00a320 - \u00a340",
"improvement_type": "C",
"improvement_details": {
"improvement_number": 1
},
"improvement_category": 5,
"energy_performance_rating": 68,
"environmental_impact_rating": 83
},
{
"sequence": 3,
"typical_saving": {
"value": 51,
"currency": "GBP"
},
"indicative_cost": "\u00a3600 - \u00a31,500",
"improvement_type": "Y",
"improvement_details": {
"improvement_number": 49
},
"improvement_category": 5,
"energy_performance_rating": 70,
"environmental_impact_rating": 83
}
],
"co2_emissions_potential": 1.0,
"energy_rating_potential": 70,
"lighting_cost_potential": {
"value": 29,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 308,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 5719.21,
"space_heating_existing_dwelling": 1270.63
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 367,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0342",
"energy_consumption_potential": 214,
"environmental_impact_current": 78,
"current_energy_efficiency_band": "E",
"environmental_impact_potential": 83,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 41,
"low_energy_fixed_lighting_bulbs_count": 5,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,372 @@
{
"uprn": 200004017091,
"roofs": [
{
"description": "Pitched, 100 mm loft insulation",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"walls": [
{
"description": "Timber frame, as built, insulated (assumed)",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "Solid, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 1,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"addendum": {
"addendum_numbers": [
15
]
},
"lighting": {
"description": "Good lighting efficiency",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"postcode": "BN14 9EL",
"hot_water": {
"description": "From main system, no cylinder thermostat",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"post_town": "WORTHING",
"built_form": 3,
"created_at": "2026-04-21 13:18:06",
"door_count": 2,
"region_code": 14,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 0,
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 26,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "Y",
"heat_emitter_type": 1,
"emitter_temperature": 0,
"main_heating_number": 1,
"main_heating_control": 2106,
"main_heating_category": 2,
"main_heating_fraction": 1,
"central_heating_pump_age": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 17803
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.1,
"window_height": 1.2,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.1,
"window_height": 1.2,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.1,
"window_height": 1.2,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.8,
"window_height": 0.8,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.5,
"window_height": 0.8,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Boiler and radiators, mains gas",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "End-terrace house",
"language_code": 1,
"pressure_test": 4,
"property_type": 0,
"address_line_1": "3 Radbone Close",
"assessment_type": "RdSAP",
"completion_date": "2026-04-21",
"inspection_date": "2026-04-20",
"extensions_count": 0,
"measurement_type": 1,
"total_floor_area": 65,
"transaction_type": 1,
"conservatory_type": 1,
"heated_room_count": 4,
"registration_date": "2026-04-21",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 2,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"extract_fans_count": 1,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 280,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 5,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.37,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 32.38,
"quantity": "square metres"
},
"party_wall_length": {
"value": 7.42,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 16.15,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.35,
"quantity": "metres"
},
"total_floor_area": {
"value": 32.37,
"quantity": "square metres"
},
"party_wall_length": {
"value": 7.42,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 16.15,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "G",
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": 2,
"roof_insulation_thickness": "100mm",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 4,
"heating_cost_current": {
"value": 556,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 2.2,
"energy_rating_average": 60,
"energy_rating_current": 71,
"lighting_cost_current": {
"value": 51,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, room thermostat and TRVs",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 516,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 333,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 71,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 44,
"currency": "GBP"
},
"indicative_cost": "\u00a35,000 - \u00a310,000",
"improvement_type": "W2",
"improvement_details": {
"improvement_number": 58
},
"improvement_category": 5,
"energy_performance_rating": 73,
"environmental_impact_rating": 74
},
{
"sequence": 2,
"typical_saving": {
"value": 143,
"currency": "GBP"
},
"indicative_cost": "\u00a34,000 - \u00a37,000",
"improvement_type": "N",
"improvement_details": {
"improvement_number": 19
},
"improvement_category": 5,
"energy_performance_rating": 76,
"environmental_impact_rating": 79
},
{
"sequence": 3,
"typical_saving": {
"value": 212,
"currency": "GBP"
},
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 81,
"environmental_impact_rating": 80
}
],
"co2_emissions_potential": 1.5,
"energy_rating_potential": 81,
"lighting_cost_potential": {
"value": 51,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 186,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 3562.39,
"space_heating_existing_dwelling": 4593.09
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 184,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0342",
"energy_consumption_potential": 118,
"environmental_impact_current": 72,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 80,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 33,
"low_energy_fixed_lighting_bulbs_count": 12,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,70 @@
# Elmhurst validation — UPRN 200004296092 (corpus, slice-5 wall fix)
Built in accredited Elmhurst RdSAP10-Online on the lodged register inputs
(end-terrace house, solid brick + **external insulation, thickness "NI"**,
pitched 150 mm loft, solid ground floor, mains-gas combi, TFA 62).
Artefacts: `elmhurst_summary.pdf` (Input Summary), `elmhurst_worksheet.pdf`
(SAP Worksheets).
## Headline
- **Lodged SAP 71**; **engine 71.74** (post slice-5 §5.4 100 mm fix, was 64.35);
**accredited Elmhurst current SAP 66** (band D), potential 75.
- Slice 5 is **directionally validated**: the externally-insulated wall must be
billed insulated, not at the uninsulated 1.40 the §5.8 gate produced. Both
Elmhurst (0.48) and the engine (0.29) are far below 1.40.
## Fabric line-by-line (engine dive vs Elmhurst worksheet)
| Element | Engine | Elmhurst | Note |
|---|---|---|---|
| External wall | 21.1 W/K (U 0.29) | 30.7 W/K (U **0.48**, "SolidWallDensePlaster**200**") | see WALL below |
| Roof | 9.35 W/K (U 0.30) | 9.35 W/K (U 0.30) | **exact match** |
| Ground floor | 21.2 W/K | 22.7 W/K (U 0.73) | close |
| Party wall | **8.57 W/K** | **0.00 W/K** | see PARTY below |
| Windows | 7.96 W/K | ~same area (4.31 + 4×2.89 split) | ~ |
The wall (engine 9.6) and party wall (engine +8.6) differences nearly cancel,
so the two HTCs are close; the residual SAP gap (71.7 vs 66) is dominated by the
heating system — this build used a GENERIC Post-98 condensing combi, whereas the
engine uses the lodged PCDB 16137 (Ideal Logic combi 30). Not a clean 1e-4
comparison for that reason.
## Leads (both partly confounded by an imperfect build — see caveats)
1. **WALL — base thickness on an unmeasured wall.** The cert lodges
`wall_thickness 360` but `wall_thickness_measured = N`. Elmhurst modelled a
**~200 mm** base wall ("SolidWallDensePlaster200") — but note my build did
NOT enter a wall thickness, so Elmhurst used its own default; had I keyed
360 mm it might match the engine. The open question is genuine though:
RdSAP §5.3 uses the documentary/measured thickness, and when NOT measured
the engine perhaps should fall to the Table-3 default rather than trust the
unmeasured lodged 360 mm. WORTH A DEDICATED CHECK (build with the thickness
keyed both ways).
2. **PARTY WALL — NOT an engine gap.** Elmhurst rendered the party wall as
"Solid, U 0.00", but the engine's **0.25** is SPEC-CORRECT: RdSAP §5.10
Table 15 row 4 ("unable to determine, house") = 0.25. The cert lodges
`party_wall_construction = 0` (unable to determine), so 0.25 is right; the
Elmhurst 0.00 reflects how my build's party-wall dropdown resolved (a
"Solid" masonry party wall, Table 15 row 1 = 0), not the cert's "unknown".
Build artifact, not a bug.
## Build caveats (why the 71.7-vs-66 residual isn't cleanly attributable)
- Heating: built a GENERIC Post-98 condensing combi; the engine uses the lodged
PCDB 16137 (Ideal Logic combi 30) — different efficiency.
- Wall thickness: not keyed into Elmhurst (defaulted 200 mm) vs engine's 360 mm.
- Party wall: entered as a resolved type (0.00) vs the cert's "unable to
determine" (engine 0.25).
So this build's clean, confirmed results are: (a) the ROOF matches exactly
(U 0.30, 9.35 W/K), (b) slice 5's DIRECTION is validated (insulated wall, not
the uninsulated 1.40). The wall-thickness question (lead 1) is the one genuine
follow-up.
## Slice-5 calibration note (banked)
The accredited "External insulation, **Unknown** thickness" selection in Elmhurst
produced U 0.48 (≈ a 50 mm equivalent on its 200 mm base), NOT the 0.29 the
§5.4-literal "assume 100 mm" gives on the engine's 360 mm base. The 100 mm fix
nevertheless matches the LODGED corpus better (MAE 0.658 → 0.647, within held) —
lodged assessors typically knew the real (~100 mm) thickness the register then
flattens to "NI". Per the campaign rule (engine = lodged is the goal; Elmhurst
arbitrates only when lodged can't be explained from register data), the 100 mm
fix STANDS; the lodged-vs-Elmhurst gap here is explained by the register's "NI"
discarding the assessor's thickness knowledge. But the base-thickness and
party-wall leads above are genuine and independent of the insulation default.

View file

@ -0,0 +1,361 @@
{
"uprn": 200004296092,
"roofs": [
{
"description": "Pitched, 150 mm loft insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"walls": [
{
"description": "Solid brick, with external insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "Solid, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 1,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "LL16 3RL",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "DENBIGH",
"psv_count": 0,
"built_form": 3,
"created_at": "2026-05-14 14:33:05",
"door_count": 2,
"region_code": 7,
"report_type": 2,
"sap_heating": {
"number_baths": 0,
"cylinder_size": 1,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 2
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 26,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "Y",
"heat_emitter_type": 1,
"emitter_temperature": 1,
"main_heating_number": 1,
"main_heating_control": 2106,
"main_heating_category": 2,
"main_heating_fraction": 1,
"central_heating_pump_age": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 16137
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1.07,
"quantity": "m"
},
"window_height": {
"value": 1.34,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1.07,
"quantity": "m"
},
"window_height": {
"value": 1.34,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 1,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 0.47,
"quantity": "m"
},
"window_height": {
"value": 0.85,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 1,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 0.47,
"quantity": "m"
},
"window_height": {
"value": 0.85,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 1,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 0.76,
"quantity": "m"
},
"window_height": {
"value": 0.84,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "WLS",
"main_heating": [
{
"description": "Boiler and radiators, mains gas",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "End-terrace house",
"language_code": 1,
"pressure_test": 4,
"property_type": 0,
"address_line_1": "54 Myddleton Avenue",
"assessment_type": "RdSAP",
"completion_date": "2026-05-14",
"inspection_date": "2026-05-14",
"extensions_count": 0,
"measurement_type": 1,
"open_flues_count": 0,
"total_floor_area": 62,
"transaction_type": 1,
"conservatory_type": 1,
"heated_room_count": 3,
"other_flues_count": 0,
"registration_date": "2026-05-14",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 2,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "true",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "true"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"closed_flues_count": 0,
"extract_fans_count": 0,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 360,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 3,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": 2.4,
"floor_insulation": 1,
"total_floor_area": 31.16,
"party_wall_length": 6.79,
"floor_construction": 1,
"heat_loss_perimeter": 15.97
},
{
"floor": 1,
"room_height": 2.4,
"total_floor_area": 31.16,
"party_wall_length": 6.79,
"heat_loss_perimeter": 15.97
}
],
"wall_insulation_type": 1,
"construction_age_band": "B",
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": 2,
"roof_insulation_thickness": "150mm",
"wall_insulation_thickness": "NI"
}
],
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 589,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 1.8,
"energy_rating_average": 60,
"energy_rating_current": 71,
"lighting_cost_current": {
"value": 50,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, room thermostat and TRVs",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"blocked_chimneys_count": 0,
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 543,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 275,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"schema_version_current": "LIG-21.0",
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 47,
"indicative_cost": "\u00a35,000 - \u00a310,000",
"improvement_type": "W2",
"improvement_details": {
"improvement_number": 58
},
"improvement_category": 5,
"energy_performance_rating": 72,
"environmental_impact_rating": 79
},
{
"sequence": 2,
"typical_saving": 275,
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 84,
"environmental_impact_rating": 81
}
],
"co2_emissions_potential": 1.4,
"energy_rating_potential": 84,
"lighting_cost_potential": {
"value": 50,
"currency": "GBP"
},
"schema_version_original": "LIG-21.0",
"hot_water_cost_potential": {
"value": 275,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 1768.31,
"space_heating_existing_dwelling": 5933.75
},
"draughtproofed_door_count": 2,
"energy_consumption_current": 169,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "10.2.2.0",
"energy_consumption_potential": 130,
"environmental_impact_current": 77,
"cfl_fixed_lighting_bulbs_count": 0,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 81,
"led_fixed_lighting_bulbs_count": 12,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 29,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,254 @@
Summary Information
Surveyor: P960-0001
Name: Richard Matthew Ratcliff Title: Mr. Tel Number: 07760 443 469
Survey Reference: 001431 My Reference: Khalim-test
Current SAP rating: D 66 Potential SAP rating: C 75 Emissions (t/year): 1.927 tonnes
Current EI rating: C 72 Potential EI rating: C 76 Fuel Bill: £1026
Property Details:
RdSAP version: RdSAP10
Reference Number: P960-0001-001431
My Reference: Khalim-test
Lodgement Required: No
Regs Region: England
EPC Language: English
UPRN:
Postcode: W6 9BF
Region: Thames Valley
House Name: 28 Distillery Wharf
House No: 28
Street: Distillery Wharf
Locality:
Town: London
County:
Property Tenure: Owner-occupied
Transaction Type: Marketed sale
Inspection Date: 25/06/2025
Process date: 02/07/2026
Check for the existence of No
an EPC:
Does an EPC exist at the No
point of carrying out this
energy assessment:
Reason why another energy
assessment needs to be
undertaken:
RdSAP Inputs
Property Description:
1.0 Property type: H House
E End-Terrace
2.0 Number of Storeys: 2
Habitable Rooms: 3
Heated Habitable Rooms: 3
3.0 Date Built: Main Property B 1900-1929
4.0 Dimensions:
Dimension type: Internal
Main Property
Floor Room Heat Loss Party Wall
Area Height Wall Perimeter Length
[m2] [m] [m] [m]
1st Floor: 31.16 2.40 15.97 6.79
Lowest Floor: 31.16 2.40 15.97 6.79 No
5.0 Conservatory:
Is there a conservatory? No
7.0 Walls:
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
Summary Information
Main Property
Type SO Solid Brick
Insulation E External
Insulation Thickness Unknown
Conductivity Known No
Thermal Conductivity Lambda = 0.04 W/m*K
Wall Thickness Unknown No
Wall Thickness 260 mm
U-value Known No
Party Wall Type S Solid masonry / timber / system build
8.0 Roofs:
Main Property
Type PA Pitched (slates/tiles), access to loft
Insulation J Joists
Insulation Thickness 150 mm
U-value Known No
8.1 Rooms in Roof:
9.0 Floors:
Main Property
Location G Ground floor
Type S Solid
Insulation A As built
Default U-value 0.73
U-value Known No
10.0 Doors:
Total Number of Doors 2
Number of Insulated Doors 0
11.0 Windows:
Frame Frame Glazing Building U g Draught Permanent
W H Area Glazing Type Location Orient. Data-Source
Type Factor Gap Part value value Proofed Shutters
Double with unknown
4.31 1.00 4.31 PVC 0.70 12 mm Main External wall North Manufacturer 2.80 0.76 Yes None
install date
Double with unknown
2.89 1.00 2.89 PVC 0.70 12 mm Main External wall West Manufacturer 2.80 0.76 Yes None
install date
Double with unknown
2.89 1.00 2.89 PVC 0.70 12 mm Main External wall North Manufacturer 2.80 0.76 Yes None
install date
Double with unknown
2.89 1.00 2.89 PVC 0.70 12 mm Main External wall East Manufacturer 2.80 0.76 Yes None
install date
Draught Proofing 67 %
12.0 Ventilation & Cooling
No. of open chimneys 0
No. of open flues 0
No. of open chimneys/open flues attached to closed fire 0
No. of flues attached to solid fuel boiler 0
No. of open flues attached to other heater 0
No. of blocked chimneys 0
No. of intermittent extract fans 0
No. of passive vents 0
No. of flueless gas fires 0
Fixed Space Cooling No
Draught Lobby Unable to determine
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
Summary Information
12.1 Mechanical Ventilation
Mechanical Ventilation No
12.2 Air Pressure Test
Test Method Not available
13.0 Lighting
Total number of bulbs 6
Number of LED and CFL Known Yes
Number of LED lights 0
Number of CFL lights 0
Total number of Low Energy 0
Total number of incandescents 6
14.0 Main Heating1
PCDF boiler Reference 0
Main Heating EES Code BGW
Main Heating SAP Code 104
Heat Emitter Radiators
Heat pump age 2013 or later
Flue Type Balanced
Fan Assisted Flue Yes
Design flow temperature <= 35°C
PCDF Heating Controls 0
Main Heating Controls EES CBE
Main Heating Controls Sap SAP code 2106, Programmer, room thermostat and TRVs
PCDF Compensator 0
Percentage of Heat 100 %
14.1 Main Heating2
PCDF boiler Reference 0
Main Heating EES Code
Main Heating SAP Code 0
Percentage of Heat 0%
14.1 Community Heating/Heat Network
Heating Type None
14.2 Meters
Electricity meter type Single
Main gas Yes
Electricity Smart Meter Present No
Gas Smart Meter Present No
15.0 Water Heating
Water Heating Code HWP
Water Heating SapCode 901
Water Heating Fuel Type Mains gas
15.1 Hot Water Cylinder
Hot Water Cylinder Present No
15.2 Community Hot Water
PCDF boiler Reference 0
16.0 Solar water heating
Solar Water Heating No
17.0 Waste Water Heat Recovery System
Is WWHRS present in the property? No / Unknown
1x.0 Baths and Showers
Total Number of Baths 2
Number of Baths Connected 0
Description Type Connected
1 Non-electric shower None
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
Summary Information
18.0 Flue Gas Heat Recovery System
Present No
19.0 Photovoltaic Panel
Photovoltaic Panel None
Export capable meter No
20.0 Wind Turbine
Terrain Type Urban
Wind turbine present? No
22.0 Special Features
21.0 Small-Scale Hydro
Electricity generated [kWh/year] 0.00
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
Summary Information
Recommendations
Loft insulation (SAP increase too small)
Flat roof insulation (Not applicable)
Room-in-roof insulation (Not applicable)
Cavity wall insulation (Not applicable)
Solid wall insulation (SAP increase too small)
Floor insulation (solid floor) (Recommended)
Hot water cylinder insulation (Not applicable)
Draught proofing (SAP increase too small)
Low energy lighting (Recommended)
Cylinder thermostat (Not applicable)
Heating controls for wet central heating system (Already installed)
Upgrade boiler, same fuel (Already installed)
Change heating to condensing gas condensing boiler (fuel switch) (Not applicable)
Flue gas heat recovery in conjunction with new boiler (Not applicable)
Solar water heating (SAP increase too small)
Heat recovery system for mixer showers (SAP increase too small)
Double glazed windows (Already installed)
Insulated doors (SAP increase too small)
Solar photovoltaic panels (Recommended)
Wind turbine (Not applicable)
PV diverter (Not applicable)
PV battery (Not applicable)
Water heating controls (Not applicable)
Alternative Recommendations
External wall insulation with cavity insulation (Not applicable)
Biomass boiler (alternative) (Not applicable)
Micro CHP (alternative) (Not applicable)
Related Party Disclosure
No related party
Addenda
When considering the PV installation consider installing PV battery and a PV diverter for water heating
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB

View file

@ -0,0 +1,384 @@
{
"uprn": 217091901,
"roofs": [
{
"description": "Flat, no insulation",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
}
],
"walls": [
{
"description": "Solid brick, as built, no insulation (assumed)",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
}
],
"floors": [
{
"description": "(another dwelling below)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 3,
"window": {
"description": "Single glazed",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
},
"lighting": {
"description": "Good lighting efficiency",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"postcode": "W11 1EP",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "LONDON",
"built_form": "NR",
"created_at": "2026-05-16 15:45:17",
"door_count": 0,
"region_code": 17,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 1,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 1
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 26,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "Y",
"heat_emitter_type": 1,
"emitter_temperature": 0,
"main_heating_number": 1,
"main_heating_control": 2107,
"main_heating_category": 2,
"main_heating_fraction": 1,
"central_heating_pump_age": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 18658
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 5,
"window_width": 1.2,
"window_height": 1.8,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 5,
"window_width": 1.2,
"window_height": 1.8,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 9,
"window_type": 2,
"glazing_type": 5,
"window_width": 0.5,
"window_height": 0.5,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 9,
"window_type": 2,
"glazing_type": 5,
"window_width": 0.7,
"window_height": 0.5,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 1,
"window_type": 1,
"glazing_type": 5,
"window_width": 1.2,
"window_height": 1.8,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 1,
"window_type": 1,
"glazing_type": 5,
"window_width": 1.1,
"window_height": 1.4,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Boiler and radiators, mains gas",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Top-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "Second Floor Flat",
"address_line_2": "216 Westbourne Park Road",
"assessment_type": "RdSAP",
"completion_date": "2026-05-16",
"inspection_date": "2026-05-16",
"extensions_count": 0,
"measurement_type": 1,
"sap_flat_details": {
"level": 3,
"top_storey": "Y",
"storey_count": 3,
"flat_location": 2,
"heat_loss_corridor": 0
},
"total_floor_area": 63,
"transaction_type": 8,
"conservatory_type": 1,
"has_draught_lobby": "true",
"heated_room_count": 3,
"registration_date": "2026-05-16",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 2,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "true",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "true"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 280,
"floor_heat_loss": 6,
"roof_construction": 1,
"wall_construction": 3,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.85,
"quantity": "metres"
},
"total_floor_area": {
"value": 63.29,
"quantity": "square metres"
},
"party_wall_length": {
"value": 22.8,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 14,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "A",
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": 6,
"wall_insulation_thickness": "NI",
"flat_roof_insulation_thickness": "AB"
}
],
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 967,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 2.8,
"energy_rating_average": 60,
"energy_rating_current": 62,
"lighting_cost_current": {
"value": 43,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, TRVs and bypass",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"blocked_chimneys_count": 1,
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 330,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 167,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 0,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 411,
"currency": "GBP"
},
"indicative_cost": "\u00a3900 - \u00a31,200",
"improvement_type": "A2",
"improvement_details": {
"improvement_number": 45
},
"improvement_category": 5,
"energy_performance_rating": 74,
"environmental_impact_rating": 76
},
{
"sequence": 2,
"typical_saving": {
"value": 129,
"currency": "GBP"
},
"indicative_cost": "\u00a37,500 - \u00a311,000",
"improvement_type": "Q",
"improvement_details": {
"improvement_number": 7
},
"improvement_category": 5,
"energy_performance_rating": 78,
"environmental_impact_rating": 81
},
{
"sequence": 3,
"typical_saving": {
"value": 18,
"currency": "GBP"
},
"indicative_cost": "\u00a3150 - \u00a3250",
"improvement_type": "D",
"improvement_details": {
"improvement_number": 10
},
"improvement_category": 5,
"energy_performance_rating": 79,
"environmental_impact_rating": 82
},
{
"sequence": 4,
"typical_saving": {
"value": 78,
"currency": "GBP"
},
"indicative_cost": "\u00a34,500 - \u00a36,000",
"improvement_type": "O",
"improvement_details": {
"improvement_number": 8
},
"improvement_category": 5,
"energy_performance_rating": 81,
"environmental_impact_rating": 85
}
],
"co2_emissions_potential": 1.0,
"energy_rating_potential": 81,
"lighting_cost_potential": {
"value": 45,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 167,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2022.69,
"space_heating_existing_dwelling": 9246.82
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 245,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 0,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 90,
"environmental_impact_current": 59,
"cfl_fixed_lighting_bulbs_count": 2,
"current_energy_efficiency_band": "D",
"environmental_impact_potential": 85,
"led_fixed_lighting_bulbs_count": 9,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 45,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,299 @@
{
"uprn": 4510053280,
"roofs": [
{
"description": "(another dwelling above)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"walls": [
{
"description": "Solid brick, as built, no insulation (assumed)",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
{
"description": "Solid brick, as built, partial insulation (assumed)",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"floors": [
{
"description": "Solid, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 2,
"window": {
"description": "Full secondary glazing",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"lighting": {
"description": "Good lighting efficiency",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"postcode": "NE1 4DG",
"hot_water": {
"description": "From main system, no cylinder thermostat",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 4
},
"post_town": "NEWCASTLE UPON TYNE",
"built_form": "NR",
"created_at": "2026-05-24 12:41:32",
"door_count": 0,
"region_code": 1,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 1,
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 29,
"secondary_fuel_type": 29,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 29,
"heat_emitter_type": 1,
"emitter_temperature": 0,
"main_heating_number": 1,
"main_heating_control": 2203,
"main_heating_category": 4,
"main_heating_fraction": 1,
"mcs_installed_heat_pump": "false",
"central_heating_pump_age": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 100053
}
],
"immersion_heating_type": "NA",
"secondary_heating_type": 691,
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"orientation": 4,
"window_type": 1,
"glazing_type": 4,
"window_width": 1.3,
"window_height": 1.46,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 4,
"window_type": 1,
"glazing_type": 4,
"window_width": 1.3,
"window_height": 1.46,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Air source heat pump, radiators, electric",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 5
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Ground-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "64 Waterloo Street",
"assessment_type": "RdSAP",
"completion_date": "2026-05-24",
"inspection_date": "2026-05-22",
"extensions_count": 0,
"measurement_type": 1,
"sap_flat_details": {
"level": 1,
"top_storey": "N",
"storey_count": 3,
"flat_location": 0,
"heat_loss_corridor": 2,
"unheated_corridor_length": 3.5
},
"total_floor_area": 47,
"transaction_type": 8,
"conservatory_type": 1,
"has_draught_lobby": "true",
"heated_room_count": 2,
"registration_date": "2026-05-24",
"sap_energy_source": {
"mains_gas": "N",
"meter_type": 2,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 1,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "Room heaters, electric",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"lzc_energy_sources": [
9
],
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 520,
"floor_heat_loss": 7,
"roof_construction": 3,
"wall_construction": 3,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.32,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 47,
"quantity": "square metres"
},
"party_wall_length": {
"value": 8,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 14.15,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "B",
"sap_alternative_wall_1": {
"wall_area": 8.12,
"sheltered_wall": "Y",
"wall_dry_lined": "N",
"wall_thickness": 520,
"wall_construction": 3,
"wall_insulation_type": 4,
"wall_thickness_measured": "Y",
"wall_insulation_thickness": "NI"
},
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": "ND",
"roof_insulation_thickness": "ND",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 2,
"heating_cost_current": {
"value": 278,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 0.8,
"energy_rating_average": 60,
"energy_rating_current": 51,
"lighting_cost_current": {
"value": 39,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Room thermostat only",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
}
],
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 272,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 1295,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 68,
"currency": "GBP"
},
"indicative_cost": "\u00a3220 - \u00a3250",
"improvement_type": "G",
"improvement_details": {
"improvement_number": 16
},
"improvement_category": 5,
"energy_performance_rating": 53,
"environmental_impact_rating": 88
}
],
"co2_emissions_potential": 0.8,
"energy_rating_potential": 53,
"lighting_cost_potential": {
"value": 39,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 1233,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 5072.68,
"space_heating_existing_dwelling": 2884.0
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 190,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 182,
"environmental_impact_current": 88,
"current_energy_efficiency_band": "E",
"environmental_impact_potential": 88,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "E",
"co2_emissions_current_per_floor_area": 18,
"low_energy_fixed_lighting_bulbs_count": 6,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,324 @@
{
"uprn": 47084930,
"roofs": [
{
"description": "Flat, insulated",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"walls": [
{
"description": "Cavity wall, with internal insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "(other premises below)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 3,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "NE12 8AQ",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "NEWCASTLE UPON TYNE",
"built_form": "NR",
"created_at": "2026-05-06 10:59:38",
"door_count": 0,
"region_code": 1,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 1,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 1
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 26,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "Y",
"heat_emitter_type": 1,
"emitter_temperature": 0,
"main_heating_number": 1,
"main_heating_control": 2106,
"main_heating_category": 2,
"main_heating_fraction": 1,
"central_heating_pump_age": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 17815
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.23,
"window_height": 1.61,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.23,
"window_height": 1.61,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.23,
"window_height": 1.61,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.23,
"window_height": 1.61,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.23,
"window_height": 1.61,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Energy Assessor",
"country_code": "ENG",
"main_heating": [
{
"description": "Boiler and radiators, mains gas",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Top-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "83 Station Road",
"address_line_2": "Forest Hall",
"assessment_type": "RdSAP",
"completion_date": "2026-05-06",
"inspection_date": "2026-05-05",
"extensions_count": 0,
"measurement_type": 1,
"sap_flat_details": {
"level": 3,
"top_storey": "Y",
"storey_count": 2,
"flat_location": 1,
"heat_loss_corridor": 1
},
"total_floor_area": 64,
"transaction_type": 8,
"conservatory_type": 1,
"has_draught_lobby": "true",
"heated_room_count": 3,
"registration_date": "2026-05-06",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 2,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "true",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "true"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 380,
"floor_heat_loss": 3,
"roof_construction": 1,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.32,
"quantity": "metres"
},
"total_floor_area": {
"value": 64.26,
"quantity": "square metres"
},
"party_wall_length": {
"value": 18.15,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 18.15,
"quantity": "metres"
}
}
],
"wall_insulation_type": 3,
"construction_age_band": "E",
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": 6,
"wall_insulation_thickness": "100mm",
"flat_roof_insulation_thickness": "75mm"
}
],
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 623,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 2.0,
"energy_rating_average": 60,
"energy_rating_current": 74,
"lighting_cost_current": {
"value": 42,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, room thermostat and TRVs",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 553,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 207,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 69,
"currency": "GBP"
},
"indicative_cost": "\u00a3900 - \u00a31,200",
"improvement_type": "A2",
"improvement_details": {
"improvement_number": 45
},
"improvement_category": 5,
"energy_performance_rating": 76,
"environmental_impact_rating": 78
}
],
"co2_emissions_potential": 1.8,
"energy_rating_potential": 76,
"lighting_cost_potential": {
"value": 42,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 208,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2384.75,
"space_heating_existing_dwelling": 5681.26
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 170,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 154,
"environmental_impact_current": 76,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 78,
"led_fixed_lighting_bulbs_count": 9,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 31,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,315 @@
{
"uprn": 74061136,
"roofs": [
{
"description": "(another dwelling above)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"walls": [
{
"description": "Solid brick, with internal insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
{
"description": "Timber frame, with additional insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "(other premises below)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 1,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "S41 7TP",
"hot_water": {
"description": "Electric instantaneous at point of use",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 5
},
"post_town": "Chesterfield,",
"psv_count": 0,
"built_form": 4,
"created_at": "2026-05-20 13:45:13",
"door_count": 1,
"region_code": 6,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 1,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 1
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 909,
"water_heating_fuel": 29,
"cylinder_thermostat": "N",
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 29,
"storage_heaters": [
{
"index_number": 230002,
"number_of_heaters": 1,
"high_heat_retention": "true"
}
],
"fan_flue_present": "Y",
"heat_emitter_type": 0,
"emitter_temperature": "NA",
"main_heating_number": 1,
"main_heating_control": 2404,
"main_heating_category": 7,
"main_heating_fraction": 50,
"sap_main_heating_code": 409,
"main_heating_data_source": 2
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.6,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.6,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
}
],
"schema_type": "RdSAP-Schema-21.0.1",
"uprn_source": "Address Matched",
"country_code": "ENG",
"main_heating": [
{
"description": "Electric storage heaters",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 5
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Mid-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "Flat 3,",
"address_line_2": "14-20 Corporation Street,",
"assessment_type": "RdSAP",
"completion_date": "2026-05-20",
"inspection_date": "2026-05-20",
"extensions_count": 0,
"measurement_type": 1,
"open_flues_count": 0,
"sap_flat_details": {
"level": 2,
"top_storey": "N",
"storey_count": 3,
"flat_location": 1,
"heat_loss_corridor": 2,
"unheated_corridor_length": 6
},
"total_floor_area": 24,
"transaction_type": 1,
"conservatory_type": 2,
"heated_room_count": 1,
"other_flues_count": 0,
"registration_date": "2026-05-20",
"sap_energy_source": {
"mains_gas": "N",
"meter_type": 3,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "Portable electric heaters (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"closed_flues_count": 0,
"extract_fans_count": 0,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 300,
"floor_heat_loss": 3,
"roof_construction": 3,
"wall_construction": 3,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": 2.4,
"total_floor_area": 24,
"party_wall_length": 8,
"heat_loss_perimeter": 12
}
],
"wall_insulation_type": 3,
"construction_age_band": "B",
"sap_alternative_wall_1": {
"wall_area": 14.4,
"sheltered_wall": "Y",
"wall_dry_lined": "N",
"wall_thickness": 150,
"wall_construction": 5,
"wall_insulation_type": 3,
"wall_thickness_measured": "Y",
"wall_insulation_thickness": "NI"
},
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": "ND",
"roof_insulation_thickness": "ND",
"wall_insulation_thickness": "measured",
"wall_insulation_thickness_measured": 25,
"wall_insulation_thermal_conductivity": 3
}
],
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"solar_water_heating": "N",
"habitable_room_count": 1,
"heating_cost_current": {
"value": 348,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 0.4,
"energy_rating_average": 60,
"energy_rating_current": 73,
"lighting_cost_current": {
"value": 23,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Controls for high heat retention storage heaters",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"blocked_chimneys_count": 0,
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 348,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 344,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"schema_version_current": "LIG-21.0",
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 66,
"indicative_cost": "\u00a3600 - \u00a31,500",
"improvement_type": "Y",
"improvement_details": {
"improvement_number": 49
},
"improvement_category": 5,
"energy_performance_rating": 76,
"environmental_impact_rating": 92
}
],
"co2_emissions_potential": 0.4,
"energy_rating_potential": 76,
"lighting_cost_potential": {
"value": 23,
"currency": "GBP"
},
"schema_version_original": "LIG-21.0",
"hot_water_cost_potential": {
"value": 278,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 1079.39,
"space_heating_existing_dwelling": 2013.05
},
"draughtproofed_door_count": 1,
"energy_consumption_current": 200,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "10.2.2.0",
"energy_consumption_potential": 187,
"environmental_impact_current": 91,
"cfl_fixed_lighting_bulbs_count": 0,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 92,
"led_fixed_lighting_bulbs_count": 6,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 19,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,507 @@
{
"der": 3.23,
"ter": 13.27,
"dfee": 35.2,
"dper": 41.21,
"tfee": 36.7,
"tper": 70.27,
"uprn": 10094615101,
"roofs": [
{
"description": {
"value": "Average thermal transmittance 0.09 W/m\u00b2K",
"language": "1"
},
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
}
],
"walls": [
{
"description": {
"value": "Average thermal transmittance 0.19 W/m\u00b2K",
"language": "1"
},
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
}
],
"floors": [
{
"description": {
"value": "(other premises below)",
"language": "1"
},
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": "ND",
"windows": {
"description": {
"value": "High performance glazing",
"language": "1"
},
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"lighting": {
"description": {
"value": "Good lighting efficiency",
"language": "1"
},
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"postcode": "TN37 7GF",
"data_type": 2,
"hot_water": {
"description": {
"value": "From main system, waste water heat recovery",
"language": "1"
},
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"post_town": "ST LEONARDS-ON-SEA",
"built_form": 1,
"created_at": "2025-03-04 11:13:23",
"living_area": 28.88,
"orientation": 2,
"region_code": 14,
"report_type": 3,
"sap_heating": {
"number_baths": 1,
"thermal_store": 1,
"shower_outlets": [
{
"shower_wwhrs": 2,
"shower_flow_rate": 8,
"shower_outlet_type": 3
}
],
"water_fuel_type": 39,
"water_heating_code": 914,
"instantaneous_wwhrs": {
"wwhrs_index_number1": 80140
},
"main_heating_details": [
{
"has_fghrs": "false",
"main_fuel_type": 39,
"storage_heaters": [
{
"index_number": 230003,
"number_of_heaters": 4,
"high_heat_retention": "true"
}
],
"main_heating_code": 409,
"main_heating_number": 1,
"is_condensing_boiler": "false",
"main_heating_control": 2404,
"main_heating_category": 7,
"main_heating_fraction": 1,
"main_heating_data_source": 3,
"is_oil_pump_in_heated_space": "false",
"is_main_heating_hetas_approved": "false"
},
{
"has_fghrs": "false",
"main_fuel_type": 39,
"main_heating_number": 2,
"main_heating_control": 2100,
"main_heating_category": 4,
"main_heating_fraction": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 104014,
"is_oil_pump_in_heated_space": "false",
"is_main_heating_hetas_approved": "false"
}
],
"has_hot_water_cylinder": "false",
"has_cylinder_thermostat": "true",
"has_fixed_air_conditioning": "false",
"secondary_heating_category": 1,
"is_immersion_for_summer_use": "false",
"is_heat_pump_installed_to_mis": "false",
"is_hot_water_separately_timed": "true",
"main_heating_systems_interaction": 2,
"is_heat_pump_assisted_by_immersion": "false"
},
"sap_version": 10.2,
"schema_type": "SAP-Schema-19.1.0",
"uprn_source": "Address Matched",
"country_code": "ENG",
"main_heating": [
{
"description": {
"value": "Electric storage heaters",
"language": "1"
},
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 5
}
],
"sap_lighting": [
[
{
"lighting_power": 10,
"lighting_outlets": 18,
"lighting_efficacy": 90
}
]
],
"terrain_type": 2,
"air_tightness": {
"description": {
"value": "Air permeability [AP50] = 4.7 m\u00b3/h.m\u00b2 (as tested)",
"language": "1"
},
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"dwelling_type": "Top-floor flat",
"language_code": 1,
"property_type": 2,
"pv_connection": 2,
"address_line_1": "7 Tyler House",
"address_line_2": "51 Potter Street",
"assessment_date": "2025-03-04",
"assessment_type": "SAP",
"completion_date": "2025-03-04",
"inspection_date": "2025-03-04",
"sap_ventilation": {
"psv_count": 0,
"wall_type": 1,
"pressure_test": 1,
"wet_rooms_count": 1,
"air_permeability": 4.65,
"open_flues_count": 0,
"ventilation_type": 8,
"has_draught_lobby": "false",
"other_flues_count": 0,
"closed_flues_count": 0,
"extract_fans_count": 0,
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"sheltered_sides_count": 1,
"blocked_chimneys_count": 0,
"flueless_gas_fires_count": 0,
"mechanical_vent_duct_type": 2,
"mechanical_vent_duct_placement": 1,
"mechanical_ventilation_data_source": 1,
"mechanical_vent_system_index_number": 500298,
"mechanical_vent_duct_insulation_level": 2,
"is_mechanical_vent_approved_installer_scheme": "false"
},
"design_water_use": 1,
"sap_data_version": 10.2,
"sap_flat_details": {
"level": 3,
"storeys": 4
},
"total_floor_area": 76,
"transaction_type": 6,
"cold_water_source": 1,
"conservatory_type": 1,
"registration_date": "2025-03-04",
"sap_energy_source": {
"pv_arrays": [
{
"pitch": 1,
"peak_power": 1.26,
"orientation": 8,
"overshading": 1
}
],
"electricity_tariff": 2
},
"sap_opening_types": [
{
"name": "Window",
"type": 4,
"u_value": 1.2,
"data_source": 2,
"frame_factor": 0.7,
"glazing_type": 7,
"isargonfilled": "false",
"solar_transmittance": 0.52
},
{
"name": "Apt door",
"type": 1,
"u_value": 0.71,
"data_source": 2,
"glazing_type": 1,
"isargonfilled": "false"
}
],
"secondary_heating": {
"description": {
"value": "None",
"language": "1"
},
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"lowest_storey_area": 75.94,
"lzc_energy_sources": [
11
],
"sap_building_parts": [
{
"sap_roofs": [
{
"name": "Roof (1)",
"u_value": 0.09,
"roof_type": 2,
"total_roof_area": 75.94
},
{
"name": "Party roof 1",
"u_value": 0,
"roof_type": 4,
"total_roof_area": 20
}
],
"sap_walls": [
{
"name": "Walls (1)",
"u_value": 0.21,
"wall_type": 2,
"total_wall_area": 61.26,
"is_curtain_walling": "false"
},
{
"name": "Walls (2)",
"u_value": 0.14,
"wall_type": 2,
"total_wall_area": 24.82,
"is_curtain_walling": "false"
}
],
"sap_openings": [
{
"name": "Opening 1",
"type": "Window",
"width": 8.28,
"height": 1,
"location": "Walls (1)",
"orientation": 2
},
{
"name": "Opening 2",
"type": "Window",
"width": 3.2,
"height": 1,
"location": "Walls (1)",
"orientation": 4
},
{
"name": "Opening 4",
"type": "Apt door",
"width": 2.12,
"height": 1,
"location": "Walls (2)",
"orientation": 5
},
{
"name": "Opening 3",
"type": "Window",
"width": 2.56,
"height": 1,
"location": "Walls (1)",
"orientation": 8
}
],
"construction_year": 2023,
"sap_thermal_bridges": {
"thermal_bridges": [
{
"length": 10.1,
"psi_value": 0.08,
"psi_value_source": 1,
"thermal_bridge_type": "E2"
},
{
"length": 5.91,
"psi_value": 0.063,
"psi_value_source": 1,
"thermal_bridge_type": "E3"
},
{
"length": 24.94,
"psi_value": 0.06,
"psi_value_source": 1,
"thermal_bridge_type": "E4"
},
{
"length": 0,
"psi_value": 0.107,
"psi_value_source": 1,
"thermal_bridge_type": "E5"
},
{
"length": 0,
"psi_value": 0.076,
"psi_value_source": 1,
"thermal_bridge_type": "E5"
},
{
"length": 25.74,
"psi_value": 0.037,
"psi_value_source": 1,
"thermal_bridge_type": "E7"
},
{
"length": 10.43,
"psi_value": 0.026,
"psi_value_source": 1,
"thermal_bridge_type": "E7"
},
{
"length": 11.9,
"psi_value": 0.043,
"psi_value_source": 1,
"thermal_bridge_type": "E16"
},
{
"length": 0,
"psi_value": 0.12,
"psi_value_source": 1,
"thermal_bridge_type": "E25"
},
{
"length": 0,
"psi_value": 0.045,
"psi_value_source": 1,
"thermal_bridge_type": "P1"
},
{
"length": 2.38,
"psi_value": -0.044,
"psi_value_source": 1,
"thermal_bridge_type": "E17"
},
{
"length": 15.34,
"psi_value": 0.064,
"psi_value_source": 1,
"thermal_bridge_type": "E10"
},
{
"length": 10.4,
"psi_value": 0.053,
"psi_value_source": 1,
"thermal_bridge_type": "E12"
},
{
"length": 10.43,
"psi_value": 0.038,
"psi_value_source": 1,
"thermal_bridge_type": "E12"
},
{
"length": 0,
"psi_value": 0.05,
"psi_value_source": 1,
"thermal_bridge_type": "E18"
},
{
"length": 0,
"psi_value": 0.15,
"psi_value_source": 4,
"thermal_bridge_type": "E11"
},
{
"length": 0,
"psi_value": 0.012,
"psi_value_source": 1,
"thermal_bridge_type": "E13"
},
{
"length": 0,
"psi_value": 0.24,
"psi_value_source": 1,
"thermal_bridge_type": "P5"
}
],
"thermal_bridge_code": 5
},
"building_part_number": 1,
"sap_floor_dimensions": [
{
"storey": 1,
"u_value": 0,
"floor_type": 4,
"storey_height": 2.38,
"heat_loss_area": 0,
"total_floor_area": 75.94
}
]
}
],
"user_interface_name": "Design SAP 10",
"windows_overshading": 2,
"heating_cost_current": {
"value": 269,
"currency": "GBP"
},
"co2_emissions_current": 0.2,
"energy_rating_average": 60,
"energy_rating_current": 92,
"lighting_cost_current": {
"value": 51,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": {
"value": "Controls for high heat retention storage heaters",
"language": "1"
},
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 269,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 94,
"currency": "GBP"
},
"thermal_mass_parameter": 64,
"user_interface_version": "2.21.9",
"co2_emissions_potential": 0.2,
"energy_rating_potential": 92,
"gas_smart_meter_present": "false",
"lighting_cost_potential": {
"value": 51,
"currency": "GBP"
},
"schema_version_original": "SAP-Schema-19.1.0",
"hot_water_cost_potential": {
"value": 94,
"currency": "GBP"
},
"is_in_smoke_control_area": "unknown",
"seller_commission_report": "Y",
"energy_consumption_current": 35,
"has_fixed_air_conditioning": "false",
"is_dwelling_export_capable": "true",
"multiple_glazed_percentage": 100,
"calculation_software_version": "2.21.9",
"energy_consumption_potential": 35,
"environmental_impact_current": 97,
"current_energy_efficiency_band": "A",
"environmental_impact_potential": 97,
"electricity_smart_meter_present": "true",
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "A",
"co2_emissions_current_per_floor_area": 2.6
}

View file

@ -361,7 +361,7 @@ class PhotovoltaicArray:
@dataclass
class SapEnergySource:
mains_gas: bool
gas_connection_available: bool
meter_type: str # int in API, str (e.g. "Single") in site notes
pv_battery_count: int
wind_turbines_count: int

View file

@ -1,3 +1,21 @@
from typing import Optional
from datatypes.epc.domain.epc_property_data import MainHeatingDetail
PROPERTY_TYPE_LOOKUP = {0: "House", 1: "Bungalow", 2: "Flat", 3: "Maisonette"}
ROOF_CONSTRUCTION_LOOKUP = {}
ROOF_INSULATION_LOCATION_LOOKUP = {}
HEAT_NETWORK_MAIN_CODES: frozenset[int] = frozenset({301, 302, 303, 304})
HEAT_NETWORK_CATEGORY: int = 6
def is_heat_network_main(main: Optional[MainHeatingDetail]) -> bool:
"""True when the dwelling's main heating is connected to a shared heat
network by SAP Table 4a code (301304) or by main_heating_category 6."""
if main is None:
return False
code = main.sap_main_heating_code
if isinstance(code, int) and code in HEAT_NETWORK_MAIN_CODES:
return True
return main.main_heating_category == HEAT_NETWORK_CATEGORY

File diff suppressed because it is too large Load diff

View file

@ -201,8 +201,14 @@ class TestFromRdSapSchema20_0_0:
# dict-tolerant array reader captures the arrays.
data = load("20_0_0.json")
data["sap_energy_source"]["photovoltaic_supply"] = [
[{"pitch": 2, "peak_power": {"value": 1.14, "quantity": "kW"},
"orientation": 5, "overshading": 1}],
[
{
"pitch": 2,
"peak_power": {"value": 1.14, "quantity": "kW"},
"orientation": 5,
"overshading": 1,
}
],
[{"pitch": 2, "peak_power": 1.14, "orientation": 5, "overshading": 1}],
]
schema = from_dict(RdSapSchema20_0_0, data)
@ -263,18 +269,30 @@ class TestFromRdSapSchema21_0_0:
# the only one those certs omit is roof_insulation_thickness.)
data = load("21_0_0.json")
for window in data.get("sap_windows", []):
for k in ("frame_factor", "glazing_gap", "pvc_frame",
"window_transmission_details"):
for k in (
"frame_factor",
"glazing_gap",
"pvc_frame",
"window_transmission_details",
):
window.pop(k, None)
for k in ("wet_rooms_count", "open_chimneys_count",
"pressure_test_certificate_number", "windows_transmission_details",
"mechanical_ventilation_index_number", "mechanical_vent_duct_type",
"mechanical_vent_duct_placement", "mechanical_vent_duct_insulation",
"mechanical_vent_duct_insulation_level",
"mechanical_vent_measured_installation", "insulated_door_u_value",
"low_energy_fixed_lighting_bulbs_count",
"cfl_fixed_lighting_bulbs_count", "led_fixed_lighting_bulbs_count",
"suggested_improvements"):
for k in (
"wet_rooms_count",
"open_chimneys_count",
"pressure_test_certificate_number",
"windows_transmission_details",
"mechanical_ventilation_index_number",
"mechanical_vent_duct_type",
"mechanical_vent_duct_placement",
"mechanical_vent_duct_insulation",
"mechanical_vent_duct_insulation_level",
"mechanical_vent_measured_installation",
"insulated_door_u_value",
"low_energy_fixed_lighting_bulbs_count",
"cfl_fixed_lighting_bulbs_count",
"led_fixed_lighting_bulbs_count",
"suggested_improvements",
):
data.pop(k, None)
data["sap_energy_source"].pop("wind_turbine_details", None)
data["sap_energy_source"].pop("pv_battery_count", None)
@ -440,7 +458,9 @@ class TestFromRdSapSchema21_0_1:
assert result.open_chimneys_count == 0
def test_omitted_cfl_fixed_lighting_bulbs_count_defaults_to_zero_not_none(self) -> None:
def test_omitted_cfl_fixed_lighting_bulbs_count_defaults_to_zero_not_none(
self,
) -> None:
data = load("21_0_1.json")
data.pop("cfl_fixed_lighting_bulbs_count", None)
schema = from_dict(RdSapSchema21_0_1, data)
@ -449,7 +469,9 @@ class TestFromRdSapSchema21_0_1:
assert result.cfl_fixed_lighting_bulbs_count == 0
def test_omitted_led_fixed_lighting_bulbs_count_defaults_to_zero_not_none(self) -> None:
def test_omitted_led_fixed_lighting_bulbs_count_defaults_to_zero_not_none(
self,
) -> None:
data = load("21_0_1.json")
data.pop("led_fixed_lighting_bulbs_count", None)
schema = from_dict(RdSapSchema21_0_1, data)
@ -542,7 +564,8 @@ class TestFromRdSapSchema21_0_1:
schema,
sap_building_parts=[
dataclasses.replace(
bps[0], roof_insulation_location=1,
bps[0],
roof_insulation_location=1,
rafter_insulation_thickness="225mm",
),
*bps[1:],
@ -555,9 +578,7 @@ class TestFromRdSapSchema21_0_1:
# Assert
assert result.sap_building_parts[0].rafter_insulation_thickness == "225mm"
def test_cylinder_heat_loss_threaded(
self, schema: RdSapSchema21_0_1
) -> None:
def test_cylinder_heat_loss_threaded(self, schema: RdSapSchema21_0_1) -> None:
# Arrange — the gov API lodges the manufacturer's declared cylinder
# loss factor (kWh/day) in `sap_heating.cylinder_heat_loss` (SAP
# 10.2 §4 branch a). Previously undeclared → `from_dict` dropped it
@ -656,7 +677,9 @@ class TestFromRdSapSchema21_0_1:
def test_lighting_element_description(self, result: EpcPropertyData) -> None:
assert result.lighting is not None
assert result.lighting.description == "Low energy lighting in 50% of fixed outlets"
assert (
result.lighting.description == "Low energy lighting in 50% of fixed outlets"
)
def test_hot_water_element_description(self, result: EpcPropertyData) -> None:
assert result.hot_water is not None
@ -676,7 +699,7 @@ class TestFromRdSapSchema21_0_1:
def test_mains_gas(self, result: EpcPropertyData) -> None:
# mains_gas: "Y"
assert result.sap_energy_source.mains_gas is True
assert result.sap_energy_source.gas_connection_available is True
def test_electricity_smart_meter(self, result: EpcPropertyData) -> None:
# electricity_smart_meter_present: "true"
@ -784,16 +807,27 @@ class TestFromRdSapSchema21_0_1:
assert len(result.sap_building_parts[0].sap_floor_dimensions) == 1
def test_floor_area(self, result: EpcPropertyData) -> None:
assert result.sap_building_parts[0].sap_floor_dimensions[0].total_floor_area_m2 == 45.82
assert (
result.sap_building_parts[0].sap_floor_dimensions[0].total_floor_area_m2
== 45.82
)
def test_floor_height(self, result: EpcPropertyData) -> None:
assert result.sap_building_parts[0].sap_floor_dimensions[0].room_height_m == 2.45
assert (
result.sap_building_parts[0].sap_floor_dimensions[0].room_height_m == 2.45
)
def test_heat_loss_perimeter(self, result: EpcPropertyData) -> None:
assert result.sap_building_parts[0].sap_floor_dimensions[0].heat_loss_perimeter_m == 19.5
assert (
result.sap_building_parts[0].sap_floor_dimensions[0].heat_loss_perimeter_m
== 19.5
)
def test_party_wall_length(self, result: EpcPropertyData) -> None:
assert result.sap_building_parts[0].sap_floor_dimensions[0].party_wall_length_m == 7.9
assert (
result.sap_building_parts[0].sap_floor_dimensions[0].party_wall_length_m
== 7.9
)
# --- room-in-roof (sap_room_in_roof.room_in_roof_type_1) ---
@ -899,7 +933,6 @@ class TestFromRdSapSchema21_0_1:
assert rhi.impact_of_solid_wall_insulation_kwh == -3560.0
# ---------------------------------------------------------------------------
# Measured wall insulation thickness (`wall_insulation_thickness == "measured"`)
# ---------------------------------------------------------------------------
@ -915,7 +948,9 @@ class TestApiWallConstructionCode:
def test_gov_api_cob_code_9_remaps_to_wall_cob_7(self) -> None:
# Arrange
from datatypes.epc.domain.mapper import _api_wall_construction_code # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_wall_construction_code,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_wall_construction_code(9)
@ -927,7 +962,9 @@ class TestApiWallConstructionCode:
# Arrange — codes 1-5 already align; gov 8 (system built) is left
# for u_wall to resolve (calc WALL_PARK_HOME=8 is never dispatched);
# gov 6 (basement) is left to the basement machinery.
from datatypes.epc.domain.mapper import _api_wall_construction_code # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_wall_construction_code,
) # pyright: ignore[reportPrivateUsage]
# Act / Assert
for code in (1, 2, 3, 4, 5, 6, 8):
@ -1015,9 +1052,7 @@ class TestApiResolveSlopingCeilingThickness:
# Act — code 8, no loft-joist thickness, age B (pre-1950), but the
# sloping ceiling carries a lodged 100 mm.
resolved: object = _api_resolve_sloping_ceiling_thickness(
8, None, "B", "100mm"
)
resolved: object = _api_resolve_sloping_ceiling_thickness(8, None, "B", "100mm")
# Assert — the lodged sloping-ceiling thickness wins over the
# pre-1950 None → 0 mm fallback.
@ -1032,9 +1067,7 @@ class TestApiResolveSlopingCeilingThickness:
)
# Act — code 8, no thickness anywhere, pre-1950 age.
resolved: object = _api_resolve_sloping_ceiling_thickness(
8, None, "C", None
)
resolved: object = _api_resolve_sloping_ceiling_thickness(8, None, "C", None)
# Assert — existing Slice 57 behaviour preserved: 0 mm (U=2.30).
assert resolved == 0
@ -1047,9 +1080,7 @@ class TestApiResolveSlopingCeilingThickness:
# Act — code 8, age B (pre-1950), sloping lodged "AB" (As Built —
# categorical, NOT a measured thickness).
resolved: object = _api_resolve_sloping_ceiling_thickness(
8, None, "B", "AB"
)
resolved: object = _api_resolve_sloping_ceiling_thickness(8, None, "B", "AB")
# Assert — "AB" is not a numeric thickness, so it must NOT win; the
# Slice 57 pre-1950 None → 0 mm (U=2.30) rule still applies.
@ -1088,9 +1119,7 @@ class TestElmhurstGlazingTypeWrappedGap:
from datatypes.epc.domain.mapper import _elmhurst_glazing_type_code
# Act
code = _elmhurst_glazing_type_code(
"Double between 2002 and 2021 16 mm or"
)
code = _elmhurst_glazing_type_code("Double between 2002 and 2021 16 mm or")
# Assert — clean "Double between 2002 and 2021" → SAP10 code 3
assert code == 3
@ -1100,9 +1129,7 @@ class TestElmhurstGlazingTypeWrappedGap:
from datatypes.epc.domain.mapper import _elmhurst_glazing_type_code
# Act
code = _elmhurst_glazing_type_code(
"Double between 2002 and 2021 16 mm or 1st"
)
code = _elmhurst_glazing_type_code("Double between 2002 and 2021 16 mm or 1st")
# Assert
assert code == 3
@ -1129,7 +1156,9 @@ class TestApiFloorTypeCode:
def test_code_6_maps_to_another_dwelling_below(self) -> None:
# Arrange
from datatypes.epc.domain.mapper import _api_floor_type_str # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_floor_type_str,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_floor_type_str(6)
@ -1140,7 +1169,9 @@ class TestApiFloorTypeCode:
def test_code_7_still_maps_to_ground_floor(self) -> None:
# Arrange — regression guard: the ground-floor signal the §5 (12)
# suspended-timber rule keys on is unchanged.
from datatypes.epc.domain.mapper import _api_floor_type_str # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_floor_type_str,
) # pyright: ignore[reportPrivateUsage]
# Act / Assert
assert _api_floor_type_str(7) == "Ground floor"
@ -1158,7 +1189,9 @@ class TestApiFloorTypeCode:
# (consumed by heat_transmission's party-floor override); it is
# != "Ground floor", so the §5 (12) suspended-timber rule stays
# inert. Pre-this, code 8 raised UnmappedApiCode, blocking the cert.
from datatypes.epc.domain.mapper import _api_floor_type_str # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_floor_type_str,
) # pyright: ignore[reportPrivateUsage]
# Act / Assert — no-heat-loss signal (not None, not "Ground floor").
assert _api_floor_type_str(8) == "(another dwelling below)"
@ -1171,7 +1204,9 @@ class TestApiFloorTypeCode:
# constant U=0.7. The string is != "Ground floor" / "(another
# dwelling below)", so it is inert metadata; the U-routing is driven
# by the `is_above_partially_heated_space` floor-dimension flag.
from datatypes.epc.domain.mapper import _api_floor_type_str # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_floor_type_str,
) # pyright: ignore[reportPrivateUsage]
# Act / Assert
assert _api_floor_type_str(3) == "(other premises below)"
@ -1182,7 +1217,9 @@ class TestApiFloorTypeCode:
# only, so the cascade routes that floor to U=0.7 (§5.14) and the
# heat-transmission step keeps its area even on a flat whose
# dwelling-level exposure defaults has_exposed_floor=False.
from datatypes.epc.domain.mapper import _api_build_sap_floor_dimensions # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_build_sap_floor_dimensions,
) # pyright: ignore[reportPrivateUsage]
from datatypes.epc.schema.rdsap_schema_21_0_1 import (
SapFloorDimension as ApiSapFloorDimension,
)
@ -1217,7 +1254,9 @@ class TestApiFloorConstructionCode:
def test_code_3_maps_to_suspended_not_timber(self) -> None:
# Arrange
from datatypes.epc.domain.mapper import _api_floor_construction_str # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_floor_construction_str,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_floor_construction_str(3)
@ -1230,7 +1269,9 @@ class TestApiFloorConstructionCode:
def test_code_2_still_maps_to_suspended_timber(self) -> None:
# Arrange — regression guard: the timber code is unchanged.
from datatypes.epc.domain.mapper import _api_floor_construction_str # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_floor_construction_str,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_floor_construction_str(2)
@ -1249,7 +1290,9 @@ class TestApiFloorConstructionCode:
# floor_construction. Empirically inert: floor W/K is identical to
# any explicit construction string across all observed code-0
# certs (the heat loss is governed by floor_heat_loss, not this).
from datatypes.epc.domain.mapper import _api_floor_construction_str # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_floor_construction_str,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_floor_construction_str(0)
@ -1267,7 +1310,10 @@ class TestDefaultMissingPostTown:
def test_absent_post_town_is_defaulted_to_empty_string(self) -> None:
# Arrange
from datatypes.epc.domain.mapper import _default_missing_post_town # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_default_missing_post_town,
) # pyright: ignore[reportPrivateUsage]
doc: dict[str, object] = {"postcode": "EX31 2LE"}
# Act
@ -1278,7 +1324,10 @@ class TestDefaultMissingPostTown:
def test_present_post_town_is_untouched(self) -> None:
# Arrange — regression guard: a lodged town passes through.
from datatypes.epc.domain.mapper import _default_missing_post_town # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_default_missing_post_town,
) # pyright: ignore[reportPrivateUsage]
doc: dict[str, object] = {"post_town": "BARNSTAPLE"}
# Act
@ -1290,31 +1339,48 @@ class TestDefaultMissingPostTown:
class TestApiRoofConstructionCode:
"""`_api_roof_construction_str` maps the GOV.UK API integer
roof_construction code to the string the cascade reads ONLY for the
"sloping ceiling" cos(30°) inclined-surface factor (Slice 89). Codes
6 and 7 are neither sloping ceilings nor base-U drivers (the roof
U-value comes from the global roofs[].description), so both map to
None: code 6 = "Thatched" (its U is set by the description, not this
field) and code 7 = "(same/another dwelling above)" an internal
ceiling with no roof heat loss, the roof-side analogue of
floor_construction code 0. Empirically inert: roof W/K is identical
whether 6/7 map to None or to an explicit pitched string across all
code-6/7 certs in the 2026 sample."""
roof_construction code to the string `heat_transmission.py` reads for
two purposes: the "sloping ceiling" cos(30°) inclined-surface factor
(Slice 89), and for codes 7/9 the "another dwelling above"
substring that suppresses that building part's roof entirely (zero
heat loss). Code 6 = "Thatched" is neither: its U-value comes from
the global roofs[].description, not this field, so it maps to None
(avoids the cos(30°) false-trigger). Codes 7 and 9 both map to
"(another dwelling above)" per RdSAP 10 Specification (10-06-2025)
§5.2.5: commercial premises above (code 9) is recorded identically to
another dwelling above (code 7) both zero heat loss."""
def test_code_7_same_dwelling_above_maps_to_none(self) -> None:
def test_code_7_same_dwelling_above_suppresses_roof_heat_loss(self) -> None:
# Arrange
from datatypes.epc.domain.mapper import _api_roof_construction_str # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_roof_construction_str,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_roof_construction_str(7)
# Assert — None: no sloping-ceiling signal (avoids the cos(30°)
# false-trigger); the internal ceiling has no roof heat loss.
assert result is None
# Assert — the party-ceiling marker, so heat_transmission.py's
# per-part "another dwelling above" suppression fires.
assert result == "(another dwelling above)"
def test_code_9_another_premises_above_suppresses_roof_heat_loss(self) -> None:
# Arrange — RdSAP 10 Spec §5.2.5: commercial premises above is
# recorded as "another dwelling above", same as code 7.
from datatypes.epc.domain.mapper import (
_api_roof_construction_str,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_roof_construction_str(9)
# Assert
assert result == "(another dwelling above)"
def test_code_6_thatched_maps_to_none(self) -> None:
# Arrange
from datatypes.epc.domain.mapper import _api_roof_construction_str # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_roof_construction_str,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_roof_construction_str(6)
@ -1325,7 +1391,9 @@ class TestApiRoofConstructionCode:
def test_code_8_still_maps_to_sloping_ceiling(self) -> None:
# Arrange — regression guard: the sloping-ceiling code is unchanged.
from datatypes.epc.domain.mapper import _api_roof_construction_str # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_roof_construction_str,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_roof_construction_str(8)
@ -1345,7 +1413,9 @@ class TestApiGlazingTransmissionTable24:
def test_single_glazing_code_5_is_table_24_u_4p8(self) -> None:
# Arrange — RdSAP 21 glazing_type 5 = "single glazing"; Table 24
# row "Single / Any period" → U 4.8 (PVC/wooden), g 0.85.
from datatypes.epc.domain.mapper import _api_glazing_transmission # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_glazing_transmission,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_glazing_transmission(5, None)
@ -1356,7 +1426,9 @@ class TestApiGlazingTransmissionTable24:
def test_single_glazing_known_data_code_15_is_table_24_u_4p8(self) -> None:
# Arrange — code 15 = "single glazing, known data"; same Table 24
# Single row when no measured U is lodged on the reduced-data path.
from datatypes.epc.domain.mapper import _api_glazing_transmission # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_glazing_transmission,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_glazing_transmission(15, None)
@ -1367,7 +1439,9 @@ class TestApiGlazingTransmissionTable24:
def test_secondary_glazing_normal_emissivity_code_11_is_u_2p9(self) -> None:
# Arrange — code 11 = "secondary glazing, normal emissivity";
# Table 24 Secondary "Normal emissivity" row → U 2.9, g 0.85.
from datatypes.epc.domain.mapper import _api_glazing_transmission # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_glazing_transmission,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_glazing_transmission(11, None)
@ -1378,7 +1452,9 @@ class TestApiGlazingTransmissionTable24:
def test_secondary_glazing_low_emissivity_code_12_is_u_2p2(self) -> None:
# Arrange — code 12 = "secondary glazing, low emissivity"; Table 24
# Secondary "Low emissivity" row → U 2.2, g 0.85.
from datatypes.epc.domain.mapper import _api_glazing_transmission # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_glazing_transmission,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_glazing_transmission(12, None)
@ -1389,7 +1465,9 @@ class TestApiGlazingTransmissionTable24:
def test_triple_glazing_2002_to_2022_code_9_is_u_2p0(self) -> None:
# Arrange — code 9 = "triple glazing, installed 2002-2022"; Table 24
# "Double or triple, 2002+ (pre-2022), any gap" → U 2.0, g 0.72.
from datatypes.epc.domain.mapper import _api_glazing_transmission # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_glazing_transmission,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_glazing_transmission(9, None)
@ -1400,7 +1478,9 @@ class TestApiGlazingTransmissionTable24:
def test_triple_glazing_pre_2002_code_10_default_gap_is_u_2p1(self) -> None:
# Arrange — code 10 = "triple glazing, pre-2002"; Table 24 Triple
# pre-2002 12mm-gap default → U 2.1, g 0.68.
from datatypes.epc.domain.mapper import _api_glazing_transmission # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_glazing_transmission,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_glazing_transmission(10, None)
@ -1412,7 +1492,9 @@ class TestApiGlazingTransmissionTable24:
# Arrange — regression guard: the already-mapped double-glazing
# 2002+ entry (U 2.0, g 0.72) is untouched by the single/secondary/
# triple extension.
from datatypes.epc.domain.mapper import _api_glazing_transmission # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_glazing_transmission,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_glazing_transmission(2, None)
@ -1433,7 +1515,9 @@ class TestApiCascadeGlazingCodeDivergentRemap:
def test_single_glazing_code_5_remaps_to_cascade_single_slot_1(self) -> None:
# Arrange — RdSAP-21 code 5 = single glazing; cascade single slot
# is 1 (g⊥ 0.85, g_L 0.90), not cascade 5 (secondary, 0.76/0.80).
from datatypes.epc.domain.mapper import _api_cascade_glazing_type # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_cascade_glazing_type,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_cascade_glazing_type(5)
@ -1444,7 +1528,9 @@ class TestApiCascadeGlazingCodeDivergentRemap:
def test_secondary_glazing_code_4_remaps_to_cascade_secondary_slot_5(self) -> None:
# Arrange — RdSAP-21 code 4 = secondary glazing; cascade secondary
# slot is 5 (g⊥ 0.76, g_L 0.80), not cascade 4 (double low-E 0.63).
from datatypes.epc.domain.mapper import _api_cascade_glazing_type # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_cascade_glazing_type,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_cascade_glazing_type(4)
@ -1454,7 +1540,9 @@ class TestApiCascadeGlazingCodeDivergentRemap:
def test_double_pre_2002_code_1_remap_unchanged(self) -> None:
# Arrange — regression guard: the existing code-1 remap (→2) stands.
from datatypes.epc.domain.mapper import _api_cascade_glazing_type # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_cascade_glazing_type,
) # pyright: ignore[reportPrivateUsage]
# Act
result = _api_cascade_glazing_type(1)
@ -1465,7 +1553,9 @@ class TestApiCascadeGlazingCodeDivergentRemap:
def test_rdsap21_native_codes_pass_through(self) -> None:
# Arrange — codes 9-15 already coincide with the g-table's RdSAP-21
# extension slots, so they must pass through untranslated.
from datatypes.epc.domain.mapper import _api_cascade_glazing_type # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_cascade_glazing_type,
) # pyright: ignore[reportPrivateUsage]
# Act / Assert
assert _api_cascade_glazing_type(14) == 14
@ -1644,7 +1734,9 @@ class TestRdSap20_0_0ReducedFieldSynthesis:
# built_form. Build sap_ventilation with sheltered_sides from built_form
# (else the calculator defaults every dwelling to mid-terrace=2). A cert
# with an open fireplace.
from datatypes.epc.domain.mapper import _api_sheltered_sides # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_sheltered_sides,
) # pyright: ignore[reportPrivateUsage]
corpus = _load_20_0_0_corpus()
if not corpus:
@ -1653,7 +1745,8 @@ class TestRdSap20_0_0ReducedFieldSynthesis:
(
c
for c in corpus
if not c.get("sap_windows") and (c.get("open_fireplaces_count") or 0) >= 1
if not c.get("sap_windows")
and (c.get("open_fireplaces_count") or 0) >= 1
),
None,
)
@ -1691,12 +1784,13 @@ class TestRdSap20_0_0ReducedFieldSynthesis:
if cert is None:
pytest.skip("no corpus cert with instantaneous_wwhrs")
iw = cert["sap_heating"]["instantaneous_wwhrs"]
expected_baths = iw["rooms_with_bath_and_or_shower"] + iw[
"rooms_with_bath_and_mixer_shower"
]
expected_mixers = iw["rooms_with_mixer_shower_no_bath"] + iw[
"rooms_with_bath_and_mixer_shower"
]
expected_baths = (
iw["rooms_with_bath_and_or_shower"] + iw["rooms_with_bath_and_mixer_shower"]
)
expected_mixers = (
iw["rooms_with_mixer_shower_no_bath"]
+ iw["rooms_with_bath_and_mixer_shower"]
)
# Act
result = EpcPropertyDataMapper.from_api_response(cert)
@ -1724,8 +1818,7 @@ class TestRdSap20_0_0ReducedFieldSynthesis:
c
for c in corpus
if any(
"identifier" not in part
for part in c.get("sap_building_parts", [])
"identifier" not in part for part in c.get("sap_building_parts", [])
)
),
None,
@ -1984,7 +2077,9 @@ class TestRdSap18_0ReducedFieldSynthesis:
# percent_draughtproofed, and built_form. Build sap_ventilation with
# sheltered_sides from built_form (else the calculator defaults every
# dwelling to mid-terrace=2). A cert with an open fireplace.
from datatypes.epc.domain.mapper import _api_sheltered_sides # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_sheltered_sides,
) # pyright: ignore[reportPrivateUsage]
corpus = _load_18_0_corpus()
if not corpus:
@ -1993,7 +2088,8 @@ class TestRdSap18_0ReducedFieldSynthesis:
(
c
for c in corpus
if not c.get("sap_windows") and (c.get("open_fireplaces_count") or 0) >= 1
if not c.get("sap_windows")
and (c.get("open_fireplaces_count") or 0) >= 1
),
None,
)
@ -2031,12 +2127,13 @@ class TestRdSap18_0ReducedFieldSynthesis:
if cert is None:
pytest.skip("no corpus cert with instantaneous_wwhrs")
iw = cert["sap_heating"]["instantaneous_wwhrs"]
expected_baths = iw["rooms_with_bath_and_or_shower"] + iw[
"rooms_with_bath_and_mixer_shower"
]
expected_mixers = iw["rooms_with_mixer_shower_no_bath"] + iw[
"rooms_with_bath_and_mixer_shower"
]
expected_baths = (
iw["rooms_with_bath_and_or_shower"] + iw["rooms_with_bath_and_mixer_shower"]
)
expected_mixers = (
iw["rooms_with_mixer_shower_no_bath"]
+ iw["rooms_with_bath_and_mixer_shower"]
)
# Act
result = EpcPropertyDataMapper.from_api_response(cert)
@ -2111,7 +2208,11 @@ class TestRdSap17_1ReducedFieldSynthesis:
if not corpus:
pytest.skip("no RdSAP-Schema-17.1 corpus harvested")
cert = next(
(c for c in corpus if not c.get("sap_windows") and c.get("glazed_area") == 1),
(
c
for c in corpus
if not c.get("sap_windows") and c.get("glazed_area") == 1
),
None,
)
if cert is None:
@ -2132,7 +2233,11 @@ class TestRdSap17_1ReducedFieldSynthesis:
if not corpus:
pytest.skip("no RdSAP-Schema-17.1 corpus harvested")
cert = next(
(c for c in corpus if not c.get("sap_windows") and c.get("glazed_area") == 2),
(
c
for c in corpus
if not c.get("sap_windows") and c.get("glazed_area") == 2
),
None,
)
if cert is None:
@ -2219,7 +2324,9 @@ class TestRdSap17_1ReducedFieldSynthesis:
) -> None:
# ADR-0028: open_fireplaces_count -> chimneys, percent_draughtproofed,
# sheltered_sides from built_form.
from datatypes.epc.domain.mapper import _api_sheltered_sides # pyright: ignore[reportPrivateUsage]
from datatypes.epc.domain.mapper import (
_api_sheltered_sides,
) # pyright: ignore[reportPrivateUsage]
corpus = _load_17_1_corpus()
if not corpus:
@ -2228,7 +2335,8 @@ class TestRdSap17_1ReducedFieldSynthesis:
(
c
for c in corpus
if not c.get("sap_windows") and (c.get("open_fireplaces_count") or 0) >= 1
if not c.get("sap_windows")
and (c.get("open_fireplaces_count") or 0) >= 1
),
None,
)
@ -2261,12 +2369,13 @@ class TestRdSap17_1ReducedFieldSynthesis:
if cert is None:
pytest.skip("no corpus cert with instantaneous_wwhrs")
iw = cert["sap_heating"]["instantaneous_wwhrs"]
expected_baths = iw["rooms_with_bath_and_or_shower"] + iw[
"rooms_with_bath_and_mixer_shower"
]
expected_mixers = iw["rooms_with_mixer_shower_no_bath"] + iw[
"rooms_with_bath_and_mixer_shower"
]
expected_baths = (
iw["rooms_with_bath_and_or_shower"] + iw["rooms_with_bath_and_mixer_shower"]
)
expected_mixers = (
iw["rooms_with_mixer_shower_no_bath"]
+ iw["rooms_with_bath_and_mixer_shower"]
)
result = EpcPropertyDataMapper.from_api_response(cert)
@ -2314,16 +2423,22 @@ class TestRoomInRoofDetailedSlopeAndStudWall:
rir = cert["sap_building_parts"][0]["sap_room_in_roof"]
rir.pop("room_in_roof_type_1", None)
rir["room_in_roof_details"] = {
"slope_length_1": 7.0, "slope_height_1": 1.4,
"slope_length_2": 7.0, "slope_height_2": 1.4,
"slope_length_1": 7.0,
"slope_height_1": 1.4,
"slope_length_2": 7.0,
"slope_height_2": 1.4,
"slope_insulation_thickness_1": "100mm",
"slope_insulation_thickness_2": "100mm",
"stud_wall_length_1": 7.0, "stud_wall_height_1": 1.03,
"stud_wall_length_2": 7.0, "stud_wall_height_2": 1.03,
"stud_wall_length_1": 7.0,
"stud_wall_height_1": 1.03,
"stud_wall_length_2": 7.0,
"stud_wall_height_2": 1.03,
"stud_wall_insulation_thickness_1": "75mm",
"stud_wall_insulation_thickness_2": "75mm",
"common_wall_length_1": 8.6, "common_wall_height_1": 1.2,
"common_wall_length_2": 8.6, "common_wall_height_2": 1.2,
"common_wall_length_1": 8.6,
"common_wall_height_1": 1.2,
"common_wall_length_2": 8.6,
"common_wall_height_2": 1.2,
}
# Act
@ -2369,12 +2484,16 @@ class TestRoomInRoofType2SimplifiedQuadratic:
rir = cert["sap_building_parts"][0]["sap_room_in_roof"]
rir.pop("room_in_roof_type_1", None)
rir["room_in_roof_type_2"] = {
"gable_wall_type_1": 1, "gable_wall_length_1": 10.0,
"gable_wall_type_1": 1,
"gable_wall_length_1": 10.0,
"gable_wall_height_1": 2.0,
"gable_wall_type_2": 0, "gable_wall_length_2": 6.0,
"gable_wall_type_2": 0,
"gable_wall_length_2": 6.0,
"gable_wall_height_2": 2.0,
"common_wall_length_1": 8.0, "common_wall_height_1": 1.0,
"common_wall_length_2": 8.0, "common_wall_height_2": 1.0,
"common_wall_length_1": 8.0,
"common_wall_height_1": 1.0,
"common_wall_length_2": 8.0,
"common_wall_height_2": 1.0,
}
# Act
@ -2505,7 +2624,9 @@ class TestNonSeparatedConservatoryApiMirror19_0:
)
dwelling_part_count = len(
EpcPropertyDataMapper.from_api_response(load("19_0.json")).sap_building_parts
EpcPropertyDataMapper.from_api_response(
load("19_0.json")
).sap_building_parts
)
cert = load("19_0.json")
@ -2568,7 +2689,9 @@ class TestNonSeparatedConservatoryApiMirror20_0_0:
)
dwelling_part_count = len(
EpcPropertyDataMapper.from_api_response(load("20_0_0.json")).sap_building_parts
EpcPropertyDataMapper.from_api_response(
load("20_0_0.json")
).sap_building_parts
)
cert = load("20_0_0.json")
@ -2637,7 +2760,12 @@ def test_gov_mappers_split_non_separated_conservatory(fixture: str) -> None:
cert = load(fixture)
cert["conservatory_type"] = 4
cert["sap_building_parts"].append(
{"floor_area": 12.0, "room_height": 1, "double_glazed": "Y", "glazed_perimeter": 9.0}
{
"floor_area": 12.0,
"room_height": 1,
"double_glazed": "Y",
"glazed_perimeter": 9.0,
}
)
# Act
@ -2709,9 +2837,17 @@ def test_rdsap_mappers_carry_main_heating_controls_display(
[
(RdSapSchema17_1, EpcPropertyDataMapper.from_rdsap_schema_17_1, "17_1.json"),
(RdSapSchema18_0, EpcPropertyDataMapper.from_rdsap_schema_18_0, "18_0.json"),
(RdSapSchema20_0_0, EpcPropertyDataMapper.from_rdsap_schema_20_0_0, "20_0_0.json"),
(
RdSapSchema20_0_0,
EpcPropertyDataMapper.from_rdsap_schema_20_0_0,
"20_0_0.json",
),
(RdSapSchema19_0, EpcPropertyDataMapper.from_rdsap_schema_19_0, "19_0.json"),
(RdSapSchema21_0_0, EpcPropertyDataMapper.from_rdsap_schema_21_0_0, "21_0_0.json"),
(
RdSapSchema21_0_0,
EpcPropertyDataMapper.from_rdsap_schema_21_0_0,
"21_0_0.json",
),
],
)
def test_rdsap_mappers_map_mechanical_ventilation_kind(
@ -2738,7 +2874,11 @@ def test_rdsap_mappers_map_mechanical_ventilation_kind(
"schema_cls, mapper, fixture",
[
(RdSapSchema19_0, EpcPropertyDataMapper.from_rdsap_schema_19_0, "19_0.json"),
(RdSapSchema21_0_0, EpcPropertyDataMapper.from_rdsap_schema_21_0_0, "21_0_0.json"),
(
RdSapSchema21_0_0,
EpcPropertyDataMapper.from_rdsap_schema_21_0_0,
"21_0_0.json",
),
],
)
def test_rdsap_19_0_21_0_0_construct_sap_ventilation_block(
@ -2755,3 +2895,109 @@ def test_rdsap_19_0_21_0_0_construct_sap_ventilation_block(
result = mapper(schema)
assert result.sap_ventilation.sheltered_sides == 1
# ---------------------------------------------------------------------------
# ADR-0028 single-glazed honouring on the reduced-field "ND" fallback.
#
# A reduced-field cert whose numeric `multiple_glazing_type` is the "ND" (Not
# Defined) sentinel used to default to double glazing (cascade code 2) on every
# pre-SAP10 synthesis seam — over-rating a genuinely single-glazed dwelling AND
# hiding it from the glazing generator (which upgrades only single-glazed
# windows). The 16.x normaliser already honoured an explicit "Single glazed"
# window description; these tests pin that same rule now firing on the 17.0 /
# 17.1 / 18.0 / 19.0 / 20.0.0 seams. Motivating cert: 2780-3922-3202-6042-9200
# (uprn 10033526327), a single-glazed community-heated flat lodging
# multiple_glazing_type="ND", window "Single glazed", multiple_glazed_proportion 0.
# ---------------------------------------------------------------------------
_REDUCED_FIELD_SEAMS = [
(RdSapSchema17_0, EpcPropertyDataMapper.from_rdsap_schema_17_0, "17_0.json"),
(RdSapSchema17_1, EpcPropertyDataMapper.from_rdsap_schema_17_1, "17_1.json"),
(RdSapSchema18_0, EpcPropertyDataMapper.from_rdsap_schema_18_0, "18_0.json"),
(RdSapSchema19_0, EpcPropertyDataMapper.from_rdsap_schema_19_0, "19_0.json"),
(RdSapSchema20_0_0, EpcPropertyDataMapper.from_rdsap_schema_20_0_0, "20_0_0.json"),
]
@pytest.mark.parametrize("schema_cls, mapper, fixture", _REDUCED_FIELD_SEAMS)
def test_nd_glazing_with_single_glazed_description_synthesises_single(
schema_cls: Any, mapper: Any, fixture: str
) -> None:
# Arrange — force the ND + single-glazed shape onto the fixture, dropping any
# per-window array so the reduced-field synthesis seam runs.
data = load(fixture)
data["multiple_glazing_type"] = "ND"
data["multiple_glazed_proportion"] = 0
data["window"] = {
"description": "Single glazed",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1,
}
data.pop("sap_windows", None)
# Act
result = mapper(from_dict(schema_cls, data))
# Assert — synthesised as single (SAP10 cascade code 1), the code the glazing
# generator's single-glazed set {1, 15} matches, NOT the old double default 2.
assert result.sap_windows
assert all(w.glazing_type == 1 for w in result.sap_windows)
@pytest.mark.parametrize("schema_cls, mapper, fixture", _REDUCED_FIELD_SEAMS)
def test_nd_glazing_without_single_signal_keeps_double_modal_default(
schema_cls: Any, mapper: Any, fixture: str
) -> None:
# Arrange — ND with a double-glazed description and a non-zero multiple-glazed
# proportion: the honouring must NOT fire, so the DG-modal default stands.
data = load(fixture)
data["multiple_glazing_type"] = "ND"
data["multiple_glazed_proportion"] = 100
data["window"] = {
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3,
}
data.pop("sap_windows", None)
# Act
result = mapper(from_dict(schema_cls, data))
# Assert — unchanged DG-modal default (cascade code 2).
assert result.sap_windows
assert all(w.glazing_type == 2 for w in result.sap_windows)
class TestReducedFieldNdGlazingResolver:
"""Unit-level pins for the shared ND resolver (schema-independent)."""
def test_single_in_description_is_single_glazed(self) -> None:
from datatypes.epc.domain.mapper import (
_reduced_field_is_single_glazed, # pyright: ignore[reportPrivateUsage]
)
assert _reduced_field_is_single_glazed("Single glazed", 50) is True
def test_zero_multiple_glazed_proportion_is_single_glazed(self) -> None:
from datatypes.epc.domain.mapper import (
_reduced_field_is_single_glazed, # pyright: ignore[reportPrivateUsage]
)
# 0% multiple-glazed = 100% single, even if the description is ambiguous.
assert _reduced_field_is_single_glazed("Not recorded", 0) is True
def test_double_description_with_glazing_is_not_single(self) -> None:
from datatypes.epc.domain.mapper import (
_reduced_field_is_single_glazed, # pyright: ignore[reportPrivateUsage]
)
assert _reduced_field_is_single_glazed("Fully double glazed", 100) is False
def test_resolver_returns_cascade_single_or_the_seam_default(self) -> None:
from datatypes.epc.domain.mapper import (
_reduced_field_nd_glazing_type, # pyright: ignore[reportPrivateUsage]
)
assert _reduced_field_nd_glazing_type("Single glazed", 0, 2) == 1
assert _reduced_field_nd_glazing_type("Fully double glazed", 100, 2) == 2

View file

@ -137,6 +137,71 @@ class TestFromSapSchema17_1RebaselineFields:
assert result.assessment_type == "SAP"
class TestFullSapPhotovoltaics:
"""Full-SAP certs lodge measured PV under `sap_energy_source.pv_arrays`;
the mapper must carry it to the domain `photovoltaic_arrays` so the
Appendix-M generation credit isn't silently dropped."""
def test_maps_the_lodged_pv_array(self) -> None:
# sap_17_1_house.json lodges one 1.62 kWp array (pitch 3, orientation 6,
# overshading 1).
schema = from_dict(SapSchema17_1, load("sap_17_1_house.json"))
result = EpcPropertyDataMapper.from_sap_schema_17_1(schema)
arrays = result.sap_energy_source.photovoltaic_arrays
assert arrays is not None
assert len(arrays) == 1
assert arrays[0].peak_power == 1.62
class TestFullSapElectricityTariffTranslation:
"""The full-SAP `energy_tariff` code space differs from the RdSAP
`meter_type` one the calculator reads, so the mapper must translate it."""
def test_economy7_tariff_is_not_read_as_single_rate(self) -> None:
# full-SAP energy_tariff 2 = "off-peak 7 hour" (Economy 7). Passing the
# code straight through read it as RdSAP meter 2 = Single → no off-peak
# split (under-rated). It must resolve to the SEVEN_HOUR off-peak tariff.
from domain.sap10_calculator.tables.table_12a import (
Tariff,
tariff_from_meter_type,
)
from datatypes.epc.domain.mapper import _sap_17_1_meter_type
meter_type = _sap_17_1_meter_type(2)
assert tariff_from_meter_type(meter_type) is Tariff.SEVEN_HOUR
@pytest.mark.parametrize(
("energy_tariff", "expected"),
[
(1, "STANDARD"), # standard tariff → single-rate (was over-rated as E7)
(2, "SEVEN_HOUR"), # off-peak 7-hour → Economy 7
(3, "SEVEN_HOUR"), # off-peak 10-hour → dual (§12 resolves 7/10hr)
(
4,
"TWENTY_FOUR_HOUR",
), # 24-hour tariff (e.g. property 709874 — unchanged)
(5, "EIGHTEEN_HOUR"), # off-peak 18-hour — was dropped to "" → STANDARD
(None, "STANDARD"), # absent/ND → unknown → standard
],
)
def test_energy_tariff_resolves_to_the_correct_calculator_tariff(
self, energy_tariff: Any, expected: str
) -> None:
from domain.sap10_calculator.tables.table_12a import (
tariff_from_meter_type,
)
from datatypes.epc.domain.mapper import _sap_17_1_meter_type
resolved = tariff_from_meter_type(_sap_17_1_meter_type(energy_tariff))
assert resolved.name == expected
class TestFromSapSchema17_1DisplayElements:
"""Display EnergyElements the WIP mapper dropped, leaving the FE
property-details panel "Unknown" for full-SAP certs (ADR-0037). Brings
@ -222,8 +287,12 @@ class TestFromSapSchema17_1FabricDescriptions:
def test_wall_description_carries_measured_u(self, sample: EpcPropertyData) -> None:
assert sample.walls[0].description == "Average thermal transmittance 0.17 W/m²K"
def test_floor_description_carries_measured_u(self, sample: EpcPropertyData) -> None:
assert sample.floors[0].description == "Average thermal transmittance 0.13 W/m²K"
def test_floor_description_carries_measured_u(
self, sample: EpcPropertyData
) -> None:
assert (
sample.floors[0].description == "Average thermal transmittance 0.13 W/m²K"
)
def test_roof_description_carries_other_premises(
self, sample: EpcPropertyData
@ -443,7 +512,7 @@ class TestFromSapSchema17_1EnergySource:
def test_mains_gas_derived_from_heating_fuel(self, sample: EpcPropertyData) -> None:
# Main heating fuel is mains gas (code 1) → mains_gas True.
assert sample.sap_energy_source.mains_gas is True
assert sample.sap_energy_source.gas_connection_available is True
def test_lighting_outlet_counts_to_bulbs(self, sample: EpcPropertyData) -> None:
# 1 fixed outlet, 1 low-energy → 1 low-energy bulb, 0 incandescent.
@ -481,7 +550,9 @@ class TestFromSapSchema17_1Ventilation:
schema = from_dict(SapSchema17_1, load("sap_17_1.json"))
return EpcPropertyDataMapper.from_sap_schema_17_1(schema)
def test_measured_air_permeability_fed_as_ap50(self, sample: EpcPropertyData) -> None:
def test_measured_air_permeability_fed_as_ap50(
self, sample: EpcPropertyData
) -> None:
# The lodged `air_permeability` is a q50 Blower-Door result, so it feeds the
# engine's AP50 path `(18) = AP50/20 + (8)` — NOT the AP4/Pulse formula
# `0.263 × AP4^0.924` (the air-permeability AP50 fix, uprn_10093116528).
@ -492,7 +563,10 @@ class TestFromSapSchema17_1Ventilation:
def test_ventilation_type_6_is_extract(self, sample: EpcPropertyData) -> None:
# ventilation_type 6 = MEV decentralised → EXTRACT_OR_PIV_OUTSIDE.
assert sample.sap_ventilation is not None
assert sample.sap_ventilation.mechanical_ventilation_kind == "EXTRACT_OR_PIV_OUTSIDE"
assert (
sample.sap_ventilation.mechanical_ventilation_kind
== "EXTRACT_OR_PIV_OUTSIDE"
)
def test_sheltered_sides_and_wet_rooms(self, sample: EpcPropertyData) -> None:
assert sample.sap_ventilation is not None
@ -587,14 +661,18 @@ class TestFromSapSchema17_1Perimeter:
# measured exposed-wall area 87.85 m² (uniform per-storey split).
result = self._map("sap_17_1_house.json")
fds = result.sap_building_parts[0].sap_floor_dimensions
gross = sum((fd.heat_loss_perimeter_m or 0.0) * (fd.room_height_m or 0.0) for fd in fds)
gross = sum(
(fd.heat_loss_perimeter_m or 0.0) * (fd.room_height_m or 0.0) for fd in fds
)
assert gross == pytest.approx(87.85, abs=1e-2)
def test_house_routes_party_walls_to_party_length(self) -> None:
# Σ(party_length_i × height_i) must equal the measured party-wall area.
result = self._map("sap_17_1_house.json")
fds = result.sap_building_parts[0].sap_floor_dimensions
party = sum((fd.party_wall_length_m or 0.0) * (fd.room_height_m or 0.0) for fd in fds)
party = sum(
(fd.party_wall_length_m or 0.0) * (fd.room_height_m or 0.0) for fd in fds
)
assert party == pytest.approx(105.44, abs=1e-2)
def test_unknown_wall_type_fails_loud(self) -> None:
@ -671,8 +749,9 @@ class TestFromSapSchema16_2:
epc = EpcPropertyDataMapper.from_api_response(load("sap_17_0.json"))
assert isinstance(epc, EpcPropertyData)
assert epc.uprn == 10023444324
# lodged 82; engine produces 80.
assert Sap10Calculator().calculate(epc).sap_score == 80
# lodged 82; the engine now also produces 82 — the cert's lodged PV is
# credited (previously dropped by the full-SAP mapper, under-rating to 80).
assert Sap10Calculator().calculate(epc).sap_score == 82
def test_18_0_0_dispatches_via_full_sap_path(self) -> None:
# SAP-Schema-18.0.0 is the full-SAP 17.1 shape; dispatched to
@ -807,6 +886,27 @@ class TestFullSapSchema16xRouting:
assert epc.door_count == 1
assert epc.dwelling_type == "Detached house"
def test_full_sap_16_x_electricity_tariff_is_translated_not_passed_through(
self,
) -> None:
# A full-SAP 16.x cert lodges the SAP `energy_tariff` code space, not the
# RdSAP `meter_type` one. Routing it through the full-SAP mapper (via the
# assessment_type=SAP gate) must apply `_sap_17_1_meter_type`: tariff 1 =
# standard → STANDARD. Were it raw-passed to the RdSAP normaliser, code 1
# = "dual" → SEVEN_HOUR (the prime-lead over-rating bug this guards).
from domain.sap10_calculator.tables.table_12a import (
Tariff,
tariff_from_meter_type,
)
data = load("sap_16_0_full.json") # electricity_tariff = 1 (standard)
epc = EpcPropertyDataMapper.from_api_response(data)
assert tariff_from_meter_type(epc.sap_energy_source.meter_type) is (
Tariff.STANDARD
)
def test_reduced_16_x_cert_unaffected_by_full_sap_routing(self) -> None:
# Arrange — a reduced 16.2 cert (assessment_type RdSAP) must stay on the
# RdSAP path, keeping its top-level property_type.
@ -925,5 +1025,7 @@ class TestFullSapSchema16xNoFloorDimensions:
data.pop("sap_flat_details", None)
# Act / Assert
with pytest.raises(ValueError, match="Wardalls Grove.*SE14 5FB.*sap_floor_dimensions"):
with pytest.raises(
ValueError, match="Wardalls Grove.*SE14 5FB.*sap_floor_dimensions"
):
EpcPropertyDataMapper.from_api_response(data)

View file

@ -103,7 +103,7 @@ class TestFromSiteNotesExample1:
def test_mains_gas_available(self, result: EpcPropertyData) -> None:
# general.mains_gas_available: true
assert result.sap_energy_source.mains_gas is True
assert result.sap_energy_source.gas_connection_available is True
def test_electricity_smart_meter(self, result: EpcPropertyData) -> None:
# general.electricity_smart_meter: true
@ -393,7 +393,9 @@ class TestFromSiteNotesExample1:
cylinder_insulation_type="Factory fitted",
cylinder_insulation_thickness_mm=12,
shower_outlets=ShowerOutlets(
shower_outlet=ShowerOutlet(shower_outlet_type="Non-Electric Shower"),
shower_outlet=ShowerOutlet(
shower_outlet_type="Non-Electric Shower"
),
),
),
# Windows
@ -453,7 +455,7 @@ class TestFromSiteNotesExample1:
],
# Energy source
sap_energy_source=SapEnergySource(
mains_gas=True,
gas_connection_available=True,
meter_type="Single",
pv_battery_count=0,
wind_turbines_count=0,
@ -620,7 +622,10 @@ class TestFromSiteNotesFloorConstruction:
def test_floor_construction_type(self, result: EpcPropertyData) -> None:
# building_construction.floor.floor_construction: "Suspended, not timber"
assert result.sap_building_parts[0].floor_construction_type == "Suspended, not timber"
assert (
result.sap_building_parts[0].floor_construction_type
== "Suspended, not timber"
)
def test_floor_insulation_type_str(self, result: EpcPropertyData) -> None:
# building_construction.floor.floor_insulation_type: "As Built"
@ -657,7 +662,10 @@ class TestFromSiteNotesHeatingBoiler:
def test_central_heating_pump_age_str(self, result: EpcPropertyData) -> None:
# heating_and_hot_water.main_heating.central_heating_pump_age: "Unknown"
assert result.sap_heating.main_heating_details[0].central_heating_pump_age_str == "Unknown"
assert (
result.sap_heating.main_heating_details[0].central_heating_pump_age_str
== "Unknown"
)
class TestFromSiteNotesMiscTopLevel:

View file

@ -44,7 +44,9 @@ class SapHeating:
instantaneous_wwhrs: Optional[InstantaneousWwhrs]
main_heating_details: List[MainHeatingDetail]
immersion_heating_type: Union[int, str]
has_fixed_air_conditioning: str
# Nested duplicate of the outer schema's has_fixed_air_conditioning
# (already Optional below) — some certs omit it here too.
has_fixed_air_conditioning: Optional[str] = None
# ADR-0028: cylinder_insulation_type is absent in 308/1000 17.0 certs.
cylinder_insulation_type: Optional[int] = None
cylinder_thermostat: Optional[str] = None

View file

@ -44,7 +44,9 @@ class SapHeating:
instantaneous_wwhrs: Optional[InstantaneousWwhrs]
main_heating_details: List[MainHeatingDetail]
immersion_heating_type: Union[int, str]
has_fixed_air_conditioning: str
# Nested duplicate of the outer schema's has_fixed_air_conditioning
# (also made Optional below) — some certs omit it here too.
has_fixed_air_conditioning: Optional[str] = None
# ADR-0028: 325/1000 omit cylinder_insulation_type — default it.
cylinder_insulation_type: Optional[int] = None
cylinder_thermostat: Optional[str] = None
@ -247,7 +249,7 @@ class RdSapSchema17_1:
hot_water_cost_potential: CostAmount
renewable_heat_incentive: RenewableHeatIncentive
energy_consumption_current: int
has_fixed_air_conditioning: str
has_fixed_air_conditioning: Optional[str] = None
multiple_glazed_proportion: int
calculation_software_version: str
energy_consumption_potential: int

View file

@ -44,7 +44,9 @@ class SapHeating:
instantaneous_wwhrs: Optional[InstantaneousWwhrs]
main_heating_details: List[MainHeatingDetail]
immersion_heating_type: Union[int, str]
has_fixed_air_conditioning: str
# Nested duplicate of the outer schema's has_fixed_air_conditioning
# (also made Optional below) — some certs omit it here too.
has_fixed_air_conditioning: Optional[str] = None
cylinder_insulation_type: Optional[int] = None
cylinder_thermostat: Optional[str] = None
secondary_fuel_type: Optional[int] = None
@ -266,7 +268,7 @@ class RdSapSchema18_0:
hot_water_cost_potential: CostAmount
renewable_heat_incentive: RenewableHeatIncentive
energy_consumption_current: int
has_fixed_air_conditioning: str
has_fixed_air_conditioning: Optional[str] = None
multiple_glazed_proportion: int
calculation_software_version: str
energy_consumption_potential: int

View file

@ -44,7 +44,9 @@ class SapHeating:
instantaneous_wwhrs: Optional[InstantaneousWwhrs]
main_heating_details: List[MainHeatingDetail]
immersion_heating_type: Union[int, str]
has_fixed_air_conditioning: str
# Nested duplicate of the outer schema's has_fixed_air_conditioning
# (also made Optional below) — some certs omit it here too.
has_fixed_air_conditioning: Optional[str] = None
cylinder_insulation_type: Optional[int] = None
cylinder_thermostat: Optional[str] = None
secondary_fuel_type: Optional[int] = None
@ -281,7 +283,7 @@ class RdSapSchema19_0:
# 19.0-specific block, absent in 713/1000 — Optional + default.
windows_transmission_details: Optional[WindowsTransmissionDetails] = None
energy_consumption_current: int
has_fixed_air_conditioning: str
has_fixed_air_conditioning: Optional[str] = None
multiple_glazed_proportion: int
calculation_software_version: str
energy_consumption_potential: int

View file

@ -56,7 +56,9 @@ class SapHeating:
instantaneous_wwhrs: Optional[InstantaneousWwhrs]
main_heating_details: List[MainHeatingDetail]
immersion_heating_type: Union[int, str]
has_fixed_air_conditioning: str
# Nested duplicate of the outer schema's has_fixed_air_conditioning
# (already Optional below) — some certs omit it here too.
has_fixed_air_conditioning: Optional[str] = None
cylinder_insulation_type: Optional[int] = None
cylinder_thermostat: Optional[str] = None
secondary_fuel_type: Optional[int] = None

View file

@ -63,7 +63,9 @@ class SapHeating:
water_heating_fuel: int
main_heating_details: List[MainHeatingDetail]
immersion_heating_type: Union[int, str]
has_fixed_air_conditioning: str
# Nested duplicate of the outer schema's has_fixed_air_conditioning
# (also made Optional below) — some certs omit it here too.
has_fixed_air_conditioning: Optional[str] = None
instantaneous_wwhrs: Optional[InstantaneousWwhrs] = None
# Real-API certs carry shower_outlets as a list, not the synthetic
# single-object form; list elements are normalised to the wrapped
@ -459,7 +461,7 @@ class RdSapSchema21_0_0:
windows_transmission_details: Optional[WindowsTransmissionDetails] = None
cfl_fixed_lighting_bulbs_count: Optional[int] = None
energy_consumption_current: int
has_fixed_air_conditioning: str
has_fixed_air_conditioning: Optional[str] = None
multiple_glazed_proportion: int
calculation_software_version: str
energy_consumption_potential: int

View file

@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import List, Optional, Union
from .common import DescriptionV1, Measurement
@ -64,7 +64,9 @@ class SapHeating:
water_heating_fuel: int
main_heating_details: List[MainHeatingDetail]
immersion_heating_type: Union[int, str]
has_fixed_air_conditioning: str
# Nested duplicate of the outer schema's has_fixed_air_conditioning
# (also made Optional below) — some certs omit it here too.
has_fixed_air_conditioning: Optional[str] = None
instantaneous_wwhrs: Optional[InstantaneousWwhrs] = None
# Real-API certs carry shower_outlets as a list, not the synthetic single-object form;
# accept both shapes so older fixtures keep parsing. List elements
@ -510,7 +512,9 @@ class RdSapSchema21_0_1:
renewable_heat_incentive: RenewableHeatIncentive
draughtproofed_door_count: int
energy_consumption_current: int
has_fixed_air_conditioning: str
# kw_only on just this field: the class isn't kw_only overall, and several
# required fields (calculation_software_version etc.) already follow it.
has_fixed_air_conditioning: Optional[str] = field(default=None, kw_only=True)
calculation_software_version: str
energy_consumption_potential: int
environmental_impact_current: int

View file

@ -107,6 +107,20 @@ class SapVentilation:
mechanical_vent_duct_type: Optional[int] = None
@dataclass
class SapPvArray:
"""One measured photovoltaic array lodged on a full-SAP cert (under
`sap_energy_source.pv_arrays`): peak power (kWp), pitch, SAP octant
orientation (1-8), overshading code, and the connection type. Mirrors the
domain `PhotovoltaicArray` the calculator's Appendix M generation uses."""
peak_power: Optional[float] = None
pitch: Optional[int] = None
orientation: Optional[int] = None
overshading: Optional[int] = None
pv_connection: Optional[int] = None
@dataclass
class SapEnergySource:
"""Electricity tariff, on-site generation and lighting. Lighting outlet
@ -118,6 +132,7 @@ class SapEnergySource:
wind_turbine_terrain_type: Optional[int] = None
fixed_lighting_outlets_count: Optional[int] = None
low_energy_fixed_lighting_outlets_count: Optional[int] = None
pv_arrays: Optional[List[SapPvArray]] = None
@dataclass
@ -232,3 +247,13 @@ class SapSchema17_1:
has_hot_water_cylinder: Optional[str] = None
# Present for flat-type dwellings; absence means not a flat.
sap_flat_details: Optional[SapFlatDetails] = None
# Top-level PV export-capability flag (lodged by the SAP 10-era full-SAP
# schemas; absent on the SAP 2012 family, where SEG export did not exist).
# SAP 10.2 Appendix M1 zeroes the PV export credit unless the dwelling is
# connected to an export-capable meter, so dropping the flag under-rates
# any export-capable PV cert (~3 SAP on cert 0380-3044-6070-2305-5925).
# The sibling top-level `pv_connection` is deliberately NOT parsed: the
# full-SAP enum disagrees with the RdSAP/gov-API one the calculator gates
# on (full-SAP certs lodge 1 with a PV credit included; RdSAP 1 = "not
# connected" = no credit), so carrying it across would zero real arrays.
is_dwelling_export_capable: Optional[str] = None

View file

@ -39,3 +39,58 @@ than churning the table twice.
The SQLModel row is defined in `infrastructure/postgres/` so the ephemeral-Postgres tests build it
via `create_all`; the production migration is FE-owned (Drizzle ORM) and tracked in
`docs/migrations/`.
### Amendment (2026-06-30, #1361 Class B): the Lodged half is absent for a predicted Property
The original invariant — **every** `PropertyBaselinePerformance` populates **both** halves, even when
equal — assumed every Property has a record *of its own* performance to lodge: a public EPC cert (the
`epc_with_overlay` path) or a Site Notes survey. It does **not** hold for the **EPC Prediction** path
(ADR-0029/0031). A predicted Property has no record of itself — its `EpcPropertyData` is deterministic
neighbour synthesis that copies a representative comparable's structure wholesale, so the
`energy_rating_current` / band / CO2 / Primary Energy Intensity on the synthesised picture are a
*different dwelling's* lodged figures. Reading **Lodged Performance** off it manufactures a **phantom**:
a borrowed neighbour's SAP presented as this Property's government-register figure. This affected
**every** predicted Property (~12,236 rows estate-wide), not the 8 the `effective-lodged-divergence`
audit surfaced — that audit only fires when the borrowed figure lands ≥15 SAP from the Effective, so it
under-counts the phantom by three orders of magnitude.
**Decision.** The Lodged half is **optional**. When `source_path == "predicted"` there is no Lodged
Performance: `PropertyBaselinePerformance.lodged` is `None` and the four `lodged_*` columns are `NULL`.
The **Effective half is unchanged and still persisted** — a predicted Property is a first-class modelled
output (it flows through Rebaselining, Bill Derivation, and Modelling like any other; its Effective
Performance and bill block are correct and load-bearing for the FE and the plan-vs-effective audit
checks). `rebaseline_reason` stays `physical_state_changed` / `both`, which already records that the
Effective figure was scored from a changed picture.
**Principle (the boundary).** Lodged Performance requires a record **of this Property** — a lodged cert
*or* a Site Notes survey (the as-surveyed values are a real observation of this dwelling). EPC
Prediction borrows a neighbour's, so it has none. The branch is therefore `source_path == "predicted"`
**not** `physical_state_changed` (true for Site Notes and Landlord Overrides too) and **not** "no public
EPC" (Site Notes has none yet keeps a legitimate Lodged Performance). Whether the Site Notes as-surveyed
values are truly "lodged" is a separate question, deferred.
**Rejected — a sentinel (`lodged = 0`).** Considered, to make "no record" visibly present rather than an
empty cell. Rejected: `0` is a valid-looking SAP score that (a) re-trips `effective-lodged-divergence`
for every predicted Property (`|effective 0| ≥ 15`), (b) poisons every `AVG(lodged_*)` aggregate that
`NULL` is correctly excluded from, and (c) has no coherent `lodged_epc_band` enum member. The "no lodged
record" signal belongs in `NULL` (honest absence) plus the **structural provenance** already carried by
the distinct predicted-EPC slot — which the FE renders as a predicted badge — not overloaded onto the
score column.
**Consequences.**
- The four `lodged_*` columns become **nullable**. The production table is **FE-owned (Drizzle)**, so the
migration (`ALTER … DROP NOT NULL`) lands in the FE repo and **must precede** any backend write of a
`NULL` lodged, or the predicted-Property INSERT violates the constraint and aborts the batch
(ADR-0012). The SQLModel mirror in `infrastructure/postgres/` is updated to `Optional` so the
ephemeral-Postgres tests build the nullable shape.
- The fix lives in `PropertyBaselineOrchestrator.run()` — the single chokepoint both the First Run
pipeline and `applications/modelling_e2e/handler.py` call — so one change repairs both entry points.
The Rebaseliner port takes `Optional[Performance]`; for a predicted Property `physical_state_changed`
is always true, so `CalculatorRebaseliner` adopts the calculator output and never reads the absent
lodged half (the divergence-log path is the pristine-cert case only, where lodged is non-null).
- A one-time backfill (`scripts/`, dry-run default + `--apply`, idempotent) NULLs the four `lodged_*`
columns on existing predicted-source rows (~12,236). It corrects only the Lodged half; Effective, the
bill block, and `rebaseline_reason` are left intact.
- `effective-lodged-divergence` already short-circuits on `lodged_sap IS NULL`, so it goes green for
predicted Properties once backfilled. Class C of #1361 (a floor that ignores implausibly-low lodged
scores on *real* certs) is an additive guard, independent of this change.

View file

@ -0,0 +1,30 @@
# Flat roofs are scored by insulation thickness (RdSAP Table 16 col 1), not the age-band default
## Context
A **Landlord Override** naming a flat roof with a known insulation depth (`flat: 50mm` / `100mm` / `150mm`) is classified to the canonical `RoofType` value `"Flat, insulated (assumed)"`, which the roof **Simulation Overlay** resolves to `roof_construction_type='Flat'` with **no thickness**. The calculator then takes the U-value from the flat age-band default (`_FLAT_ROOF_BY_AGE`, RdSAP Table 18 column (3)). The stated depth is **discarded** — ~102 `property_overrides` rows.
The overlay's own comment asserted *"No flat RoofType value carries an explicit mm depth"*. That is a **taxonomy artefact, not a truth about the data**: the `RoofType` enum has flat members only for insulation *state* (`FLAT_INSULATED / _ASSUMED / LIMITED / NO_INSULATION`) and **no flat-thickness members** — whereas pitched has the full `PITCHED_LOFT_12MM … 400MM` ladder. So a flat depth has nowhere to land and is lost at **classification**, before the overlay ever runs.
Reviewing the **RdSAP 10 Specification (10-06-2025)** during grilling (issue #1361 override audit / #1376) settled the domain fact: **Table 16** ("Roof U-values when loft insulation thickness is known"), **column (1)**, is headed *"Insulation at joists at ceiling level **and flat roof**"*. A flat roof **is** scored by thickness — 50 mm → 0.68, 100 mm → 0.40, 150 mm → 0.30. Table 18 (the age-band default) is explicitly only the fallback *"used when thickness of insulation cannot be determined"*. The earlier assumption that flat roofs are age-band-only was wrong.
The calculator already implements this correctly: `u_roof` routes a flat roof **with** a thickness through `_ROOF_BY_THICKNESS` (which *is* Table 16 col (1)). So a flat roof given its depth scores right today — the only defect is that the depth never reaches the calculator, because the taxonomy can't carry it.
This is the opposite of the wall / flat "as built (assumed)" cases (ADR-0033), where **no** real datum exists and the age band is the correct lever. Here a **real measurement** exists and RdSAP scores it.
## Decision
Make a flat roof's known insulation thickness first-class, so it reaches Table 16 col (1):
- Add **flat-thickness members** to the `RoofType` taxonomy — `FLAT_12MM … FLAT_400_PLUS_MM` (values like `"Flat, 150 mm insulation"`**not** "loft"; a flat roof has no loft). Minimum set for today's data: 50, 100, 150; the full ladder mirrors the pitched members for symmetry.
- The roof **Simulation Overlay** emits `roof_insulation_thickness` for a flat roof with a known depth (reusing the existing mm regex), alongside `roof_construction_type='Flat'`, so `u_roof` reaches Table 16 col (1) instead of the age-band default.
- **Reclassify** the existing `flat: Nmm` rows off `"Flat, insulated (assumed)"` onto the new members.
- Correct the misleading overlay comment.
The flat/pitched distinction is retained even though Table 16 col (1) gives the same U at a given thickness for both — it stays load-bearing for non-U concerns (measure eligibility, shape), so flat depths are **not** collapsed onto the pitched members.
## Consequences
- ~102 flat-roof properties re-score from the age-band default to their true thickness-based U-value — **both directions** (a newer-band flat roof with 150 mm improves; an old-band flat with 50 mm may worsen relative to its default).
- **Cross-repo (FE-owned pgEnum, Dan).** The `RoofType` `value` column is a Drizzle-owned Postgres enum (see `main-heating-system-pgenum-is-fe-owned`; PR #1361 Class A/B). The new members must be added to the pgEnum by the FE owner before the classifier-cache `value` writes; the `property_overrides` (TEXT) reclassify is immediate. So this is an **enum-dependent slice**, grouped with the other new-archetype slices of #1376 — not the no-enum roof/glazing resolver slice.
- The reclassify follows the established one-time-script shape (dry-run default, `--apply` in a transaction, idempotent), and surfaces any members the live enum does not yet carry as deferred, exactly as Class A did.

View file

@ -0,0 +1,129 @@
# Landlord-heating classification targets a complete, modellable taxonomy; unmapped input is no-override
## Status
accepted
## Context
Landlord supplementary data describes a dwelling's components in **unbounded
free-text** — a wall is "cavity insulated" / "CWI" / "Cav filled"; a heating
system is "communal gas boiler" / "Quantum storage" / a boiler make. An LLM
classifier maps that free-text onto a **closed internal taxonomy** of recognised
descriptions, each of which binds to a Simulation Overlay applied to
`EpcPropertyData` (ADR-0032). For heating the taxonomy is the
`MainHeatingSystemType` enum, mapped to a representative SAP Table 4a/4b code by
`main_heating_system_overlay`, with coherent companions dragged from the code
(ADR-0035). The LLM is the right tool for the unbounded-free-text problem; the
deterministic dicts in legacy `asset_list` were a *reviewed cache of LLM output*,
not a replacement, and the `landlord_*_overrides` table already serves as that
verified cache (keyed `portfolio_id + description`).
The taxonomy was **too small** — 9 heating archetypes against a ~13-family RdSAP
main-heating taxonomy. With no correct target the classifier force-picked the
nearest wrong archetype, and treated **"Gas CPSU" as a garbage drawer**: oil and
solid-fuel room heaters, community heating, and `"boiler: a rated na"` all
classified to Gas CPSU; the word "convector" in `"electric (direct acting) room
heaters: panel, convector or radiant heaters"` pulled it to `"Electric storage
heaters, convector"` (code 403, off-peak storage) instead of `"Electric room
heaters"` (691, direct-acting, single-rate).
Folded into the Effective EPC, a single-rate dwelling modelled as off-peak
storage scores **SAP ~10 (band G)**. This is the true cause of **PRD #1361
Class A** — 14 properties lodged in band C/D rebaselining to band G — which had
been mis-attributed to a fabric "no insulation (assumed)" assumption and the
pre-SAP10 rebaseline. The walls are correct (landlord asserted as-built); the
heating is the crater. Exemplar **property 718066**: lodged SAP 57, landlord
described "electric (direct acting) room heaters: panel, convector or radiant"
(correct code 691, single-rate), mis-stored as code 403 → live SAP **9.9**. The
stored override was classified `source=classifier` on 2026-06-20, **before** the
691 "Electric room heaters" archetype existed (commit ada2bc07, 2026-06-29) — so
it is also stale.
## Decision
The fix is the **target taxonomy, not the mapper**. The LLM stays.
1. **The classifier targets a complete, modellable taxonomy.** Expand
`MainHeatingSystemType` + `_MAIN_HEATING_CODES` to cover every RdSAP
main-heating family the calculator can score: room heaters by fuel
(gas / oil / solid), warm air, **heat pumps**, and **community heating**. Each
maps to a representative Table 4a/4b code; coherent companions drag from the
code per ADR-0035, so adding an archetype is still "just add its code".
2. **Unmapped free-text → `None`, never a forced archetype.** The overlay already
returns `None` (no overlay → the lodged EPC heating stands) for any value not
in `_MAIN_HEATING_CODES`; the classifier must *emit* Unknown/`None` when it
cannot confidently place the text, rather than the nearest wrong archetype.
"Gas CPSU" (or any other archetype) is never a fallback. The `None` mapping is
persisted and surfaced to the user as "no suitable match" for later edit. A
forced archetype overwrites a correct lodged cert; `None` (keep lodged) is the
only safe and honest behaviour.
3. **Heat pumps are modellable without a PCDB index** — a "model unknown" heat
pump maps to a default Table 4a heat-pump code (211224) with the table's
default seasonal efficiency; no `main_heating_index_number` is required.
4. **Community heating is modelled via its explicit codes** — 301 (boiler
community) / 302 (CHP + boilers) / 304 (electric heat-pump community), which
the catalogue strings disambiguate ("community boilers only" / "community CHP
and boilers" / "community heat pump"). The calculator (Appendix C §C3.2
distribution loss, Table 12c DLF, Table 12 source factors) and Bill Derivation
(`HEAT_NETWORK` indicative rate) already support these. **Load-bearing
assumption:** when the landlord text does not supply them, the **DLF defaults
to Table 12c 1.50** and the source to a boiler — community SAP is *sensitive*
to the DLF (1.0 vs 1.5 is a large swing), so this default is documented here
and surfaced as an assumption. Coarse "communal heating" with **no named
source** falls to `None` rather than inventing a heat-network model.
5. **Re-classify the stale `source=classifier` overrides** once the taxonomy is
complete, so the band-G properties pick up their correct code.
## Considered Options
- **Replace the LLM with a deterministic catalogue map** — rejected: landlord
input is unbounded free-text, not a closed vocabulary. The LLM is the right
front door; determinism belongs as a downstream *verified* cache (the override
table), built from LLM output + review, exactly as `asset_list` was.
- **Keep the 9-member enum, tune the LLM prompt** — rejected: with no correct
target the LLM must pick a wrong archetype regardless of prompt. Target
completeness is the fix; prompt quality is secondary.
- **Force unmapped input to a conservative archetype** (e.g. direct-acting
electric) — rejected: any forced archetype overwrites a correct lodged cert.
- **Defer community heating to `None`** (grill option b) — rejected in favour of
modelling the three explicit community codes, since calculator + bill already
support heat networks and the catalogue strings disambiguate the cases; only
coarse unnamed "communal heating" falls to `None`.
This extends [ADR-0032](0032-landlord-override-epc-overlay.md) (the override
overlay) and [ADR-0035](0035-coherent-heating-system-synthesis.md) (coherent
companions drag from the code). The visible baseline shift it produces is
correct Rebaselining per [ADR-0039](0039-override-aware-rebaselining.md). It
**corrects the stated cause of PRD #1361 Class A** (classifier taxonomy gap, not
fabric/rebaseline).
### Amendment: natural-fuel coherence
A room-heater / direct-acting archetype names a *system*, not its fuel — and fuel
arrives as its own composable `main_fuel` override. Two rules keep the system
self-coherent without coupling the two overlays:
- **Every archetype drags a natural fuel so the system self-coheres without a
`main_fuel` override.** Where the fuel is unambiguous it is exact — electric
room heaters / direct-acting / storage → electricity (RdSAP `main_fuel` 29), a
gas boiler → mains gas (26), an oil room heater → oil (28). **Solid fuel is
ambiguous** (coal / anthracite / smokeless / dual fuel / wood logs / pellets),
so it **defaults to house coal (33)** — the most common solid fuel — which a
specific `main_fuel` override then refines. (The `main_fuel` override's own
vocabulary must grow to carry the full solid-fuel list — a parallel taxonomy
gap, same shape as this one.)
- **A present `main_fuel` override wins** by the applicator's last-wins
composition (`apply_simulations` setattrs non-`None` fields in order), so the
`main_fuel` overlay is applied **after** the heating overlay. The agreeing
case (electric system + electricity fuel) is order-immaterial; only a
*disagreeing* one depends on the ordering.
- **A landlord fuel that contradicts the archetype's natural fuel is *logged*,
not raised** (e.g. "gas" fuel on a solid-fuel room heater) — an
override-plausibility signal (cf. the ADR-0039 scanner); what to do with it is
deferred. The override is still honoured (the landlord wins); we only record
the implausibility.

View file

@ -0,0 +1,42 @@
# Landlord glazing override reconciles against the cert's per-window composition, not a whole-dwelling type
## Context
A **Landlord Override** for glazing is a whole-dwelling categorical (`GlazingType` — Single / Double pre- and post-2002 / Triple / Unknown). The glazing **Simulation Overlay** resolves it to one SAP10 `glazing_type` code, and `_fold_glazing` overwrites **every** window's `glazing_type` (and clears its lodged U-/g-value) — flattening the cert to a single type.
The landlord descriptions are frequently **aggregate splits** ("40% double, 60% single"), and the LLM classifier collapses *any-double-present* to `"Double glazing"`. So ~319 `property_overrides` rows describing a **single-dominant** mix are scored as **fully double-glazed** — a material over-credit to the window U-values, hence to SAP.
The Effective EPC already carries **per-window glazing**: `sap_windows`, each with its own `glazing_type` **and** `window_width`×`window_height` (so area is derivable), plus the dwelling-level `multiple_glazed_proportion`. This is **more granular** than any whole-dwelling summary, and a whole-dwelling percentage **cannot be faithfully assigned to specific windows** — "which windows are the single ones" is unknowable from an aggregate. `_fold_glazing` already holds the EPC at apply time, so the per-window data is available where the override is applied.
Fixing this by "dominant type wins" would still **clobber real per-window variation** (a 60/40 dwelling flattened to one type). The faithful move is to treat the cert's per-window data as **authoritative** and the landlord aggregate as a **check** on it — the descriptions are, after all, aggregations of the same per-window split the cert records.
## Decision
The landlord glazing override **reconciles against the cert's per-window composition** instead of imposing a whole-dwelling type.
- **Classifier emits a proportion, not just a dominant type.** The asserted glazing **proportion** (e.g. multiple-glazed %) is extracted by the **LLM** — which is there precisely because landlord inputs vary; we do **not** hard-parse one string format.
- **Compute the cert's actual composition** from `sap_windows`, **area-weighted** by window (bigger windows dominate the dwelling U-window).
- **Reconcile in `_fold_glazing`** (it holds the EPC), three outcomes:
- **Uniform assertion (~100% one type)** → apply the blanket type. Unambiguous, and a real correction (e.g. a full reglaze). Unchanged from today for clean cases.
- **Mixed, proportion within tolerance of the cert****no-op**. The cert already reflects it, per-window and more precisely — leave the better data alone.
- **Mixed, proportion materially different from the cert****no-op + flag**. The landlord genuinely disagrees, but per-window assignment is unknowable, so **do not fabricate a split** — surface it for review.
- **Tolerance** is a tunable band on the area-weighted multiple-glazed %, pinned against the real distribution, not guessed.
## Consequences
- The ~319 over-credited rows stop being flattened to double; a mix that matches the cert is left on its per-window data, and a clean uniform reglaze still applies.
- **No new enum.** A no-op reconciliation leaves the cert glazing untouched (as `Unknown` already does). But it needs a **classifier-output change** (emit a proportion) **plus** the reconcile in the **apply seam** (`_fold_glazing`) — so it is its **own slice**, distinct from the no-enum roof party-ceiling fix that ships first.
- Genuine landlord-vs-cert glazing disagreements are **surfaced** rather than silently trusted (old behaviour: flatten to double) or silently overwritten.
- **Alternatives rejected.** *Dominant type wins* — still clobbers per-window variation. *Carry a full landlord composition to re-derive per-window types* — per-window assignment from an aggregate is unknowable; we decline to fabricate it. *Deterministic regex of "X% double, Y% single"* — brittle against varied input, which is the LLM's job.
- Pairs with the per-window fidelity the calculator already relies on: the reconciliation is only correct on a faithful Effective EPC whose `sap_windows` round-trip (cf. ADR-0040).
## Amendment (#1376 implementation): the reconcile collapses to a `MIXED` sentinel
Implementing this surfaced that the full "carry a proportion → compare to the cert → flag when far" design buys less than its cost. The cert **already** carries the best-possible per-window mapping (`sap_windows`, each with its own `glazing_type`); a landlord aggregate ("40% double, 60% single") says *how much* is double but **not which windows**, so applying it would overwrite real per-window data with an arbitrary guess. Per-window assignment from an aggregate is therefore not just unknowable but strictly worse than the cert — so a mixed override **always defers**, regardless of the exact proportion. The proportion comparison only ever fed the "flag when far" branch, and carrying a proportion needs a new schema field (an FE/Drizzle dependency).
**Realized decision.** Add a `GlazingType.MIXED` sentinel. The LLM classifies a genuine aggregate-mix to `MIXED` (the LLM handles the varied phrasings — no regex); `MIXED` is absent from the overlay's `_GLAZING_CODES`, so it resolves to **no overlay** and `_fold_glazing` never runs — the cert's per-window glazing is preserved. A **uniform** assertion (one type ≳ 90%) still resolves to that type and is applied to every window (unambiguous — no guess). Existing mixed-as-double rows are reclassified to `MIXED` (a deterministic parse of the `%X / %Y` format is acceptable for fixing *known* rows). The "uniform vs mixed" threshold (~90% one type) is tunable, pinned against the real distribution.
**Consequences of the amendment.**
- The defer behaviour needs **no apply-seam logic and no proportion field** — it falls out of "no overlay". The slice is: add the `MIXED` member, give the classifier the option, reclassify the ~319 rows.
- `MIXED` is a **new FE-owned pgEnum value** (one member) — added by the FE owner (Dan); `property_overrides` (TEXT) reclassify runs immediately, the cache pgEnum write defers until the migration (the Class-A/B pattern).
- The **"flag when landlord disagrees with the cert" is dropped for now** — since a mix always defers, the flag was informational only. `MIXED` is the hook if a future slice wants to surface those disagreements.

View file

@ -0,0 +1,123 @@
# Water-heating override: community DHW is scored by its heat-network main, individual DHW by its own fuel
## Status
accepted
## Context
A **Landlord Override** for water heating names a canonical `"<system>, <fuel>"`
**Recognised Internal Description** (`WaterHeatingType`), which the water-heating
**Simulation Overlay** (`water_heating_overlay_for`) decomposes into the SAP
`water_heating_code` (Table 4a system) + `water_heating_fuel` int codes the
calculator reads (ADR-0032). The taxonomy was **too small** — it had no biomass /
wood / dual-fuel / biodiesel water fuel — so the LLM classifier funnelled every
such description into the nearest bucket, **`"From main system, house coal"`**
(fuel 33). An audit found **131** `property_overrides` rows on that value; only
**3** are genuinely house coal. This is the same missing-archetype defect as
ADR-0041 (heating) — a dumping ground standing in for a gap in the taxonomy.
Investigating the mis-score revealed the fix is **not** a uniform "swap coal for
biomass". The 131 rows split by whether the dwelling's **main heating is a heat
network**, because the calculator scores hot water differently in the two cases:
- **Community main** (SAP Table 4a 301/302/304). When DHW inherits from the main
(`water_heating_code` ∈ {901, 902, 914}) and the main is a costable heat
network, `cert_to_inputs._is_community_heating_hw_from_main` computes the hot-
water CO2/PE/cost from the **main's** heat-network fuel (via
`_heat_network_community_fuel_code`, e.g. biomass-community fuel 31 → Table 12
code 43), scaled by heat-source efficiency — and **ignores
`water_heating_fuel` entirely**. Verified: HW CO2 is `0.036` whether the water
fuel is 33 (coal) or 31 (biomass). So the **84** community rows already score
their DHW as biomass-heat-network; the `"house coal"` value is a **cosmetic
display lie**, not a scoring error.
- **Individual main** (a real boiler / room heater). DHW inherits the explicitly
lodged `water_heating_fuel`, so `33` scores as **house coal (0.395 kgCO2/kWh)**
a genuine ~14× carbon overstatement for a wood/biomass dwelling. **47** rows are
mis-scored this way (20 wood logs, 4 dual fuel, 23 "electric immersion
assumed"); **3** genuine house-coal rows score correctly.
The community-fuel collision (API enum 30/31/32 collide in value with the Table-32
electricity tariff codes 30/31/32) is **already handled** at the fuel-type
boundary, gated on heat-network context (`_heat_network_community_fuel_code`) — a
deliberate design that must **not** be globally canonicalised, because the cascade
uses bare code 30 internally as standard-rate grid electricity
(`table_32.py` comment). So **no calculator change is required**: community DHW is
scored correctly *given the coherent community main the override data already
carries* (`main_heating_system = "Community heating, boilers"`,
`main_fuel = "biomass (community)"`).
## Decision
Complete the water-heating taxonomy and reclassify the dumping-ground rows to
**faithful** archetypes. No calculator change.
1. **Add the missing water archetypes**, each mapping to its RdSAP
`water_heating_fuel` code in `water_heating_overlay._WATER_HEATING_CODES`:
- `"From main system, wood logs"` → (901, **6**)
- `"From main system, biomass (community)"` → (901, **31**)
- `"From main system, dual fuel (mineral and wood)"` → (901, **9**)
- `"From main system, biodiesel (community)"` → (901, **34**)
2. **Community DHW maps to its community fuel (31 / 34) and is score-neutral.**
Because the community main drives the HW score, mapping the 75 community
biomass/biodiesel rows to their honest community fuel corrects the **displayed
value** without changing the score — we do **not** flatten them to individual
wood (6), which would be a semantic lie. Correct labelling, zero scoring risk.
3. **Individual DHW maps to its real fuel and moves the score.** Wood-logs → 6
(0.028), dual fuel → 9, off the coal 0.395 — the genuine fix for the 47
mis-scored individual-main rows.
4. **"No hot water system present electric immersion assumed" → the existing
`"Electric immersion, electricity"` archetype (903, 29).** The description
states the assessment's own assumption; honouring it is faithful. This corrects
the 23 individual-main rows (coal → electricity) and re-labels the 9
community-main rows off the accidental biomass score onto the assessed
immersion — a small, defensible score change, surfaced here as the one non-
cosmetic community reclassification.
5. **Genuine `"From main system, house coal"` (3 rows) is retained** — the taxonomy
keeps house coal for the dwellings that really burn it.
6. **Deterministic guard + reclassify, TEXT-first / pgEnum-deferred.** A pure
`water_heating_guard(description) -> Optional[WaterHeatingType]` (mirroring
`glazing_mix_guard`) recognises the structured `"<system>: <fuel>"` phrasings
and is wired via `GuardedColumnClassifier(guard, fallback=ChatGpt…)`; varied
phrasings stay with the LLM. A one-time `scripts/reclassify_water_heating.py`
reuses the same guard as its decision function (so backfill and live path can't
drift), updates `property_overrides.override_value` (TEXT) unconditionally, and
defers the `landlord_water_heating_overrides.value` pgEnum-cache write for any
value the live enum doesn't yet carry — printing the deferred set for the FE
owner (the `main-heating-system-pgenum-is-fe-owned` constraint).
## Consequences
- **FE-owned pgEnum additions (Dan).** Four new `landlord_water_heating_overrides`
values: `"From main system, wood logs"`, `"From main system, biomass
(community)"`, `"From main system, dual fuel (mineral and wood)"`, `"From main
system, biodiesel (community)"`. The TEXT read path is fixed immediately; the
cache write for these four defers until the migration lands.
- **No `cert_to_inputs.py` change**, so no risk to the thousands of lodged certs
that carry water fuel 31/33/34 directly. (A separate latent question — whether a
lodged *individual*-main cert with `water_heating_fuel = 31` or `34` mis-scores
as electricity because those codes are un-normalised outside heat-network
context — is **out of scope** here and tracked separately; it does not affect any
override row, all of which resolve correctly under this decision.)
- Extends [ADR-0041](0041-landlord-heating-classification-targets-a-complete-modellable-taxonomy.md)
(complete taxonomy, no dumping ground) and [ADR-0035](0035-coherent-heating-system-synthesis.md)
(the community main and its DHW cohere — here the coherence is what makes the
community rows score correctly). The visible baseline shift on the 47 individual
rows is correct Rebaselining ([ADR-0039](0039-override-aware-rebaselining.md)).
### Alternatives rejected
- **Flatten community biomass to individual wood (code 6).** Scores ~identically
(0.028 vs 0.029) but mislabels a district-heating dwelling as burning its own
wood — a semantic lie for no benefit, since the community main already scores it.
- **A calculator "collision fix" normalising 31/33/34 globally.** Rejected: 33 is
already normalised (`canonical_fuel_code` 33→11); 31/32 must stay un-normalised
outside heat-network context or genuine grid electricity mis-prices as community
waste (`table_32.py`). No override row needs it.
- **Defer the community rows to `None`.** Rejected per ADR-0041 dec-4 (community is
modelled, not deferred) — and unnecessary, since they already score correctly.

View file

@ -0,0 +1,94 @@
# A baseline HHRSH override drags its intrinsic SAP-409 controls, not the generic manual storage default
## Status
accepted
## Context
A **Landlord Override** naming *"High heat retention storage heaters"* (HHRSH) had
no home in the heating taxonomy: `MainHeatingSystemType` carried
`ELECTRIC_STORAGE_OLD` (SAP 401), `_SLIMLINE` (402), `_CONVECTOR` (403) and
`_FAN` (404), but no High-Heat-Retention member. So the LLM classifier funnelled
the **best modern storage** into **`"Electric storage heaters, old"` (401)** — the
**worst** large-volume type. An audit found **111** `property_overrides` rows on
that mapping (`'Electric Storage Systems: High heat retention storage heaters'`
`'Electric storage heaters, old'`). Same missing-archetype defect as ADR-0041.
HHRSH is a distinct, modellable RdSAP system: **SAP Table 4a code 409** (high heat
retention storage heaters), which the calculator already scores
(`sap_efficiencies` carries `409: 1.00`). The **HHRSH *measure*** already installs
this exact code (`_HHR_STORAGE_SAP_CODE = 409`,
`domain/modelling/generators/heating_recommendation.py`) on a **Dual** off-peak
meter with **control 2404**.
The subtlety is the **charge control**. `main_heating_system_overlay._control_for`
drags the **conservative manual** charge control (`2401`) for *every* storage code
401409, following ADR-0041's amendment ("an *unobserved* storage charge control
defaults to the conservative manual one"). Table 4e Group 4's mean-internal-
temperature adjustments are **manual +0.7 °C, automatic/Celect +0.4 °C, HHR 0 °C**
— so a manual default carries a penalty. Applying it to a 409 heater would swap
"scored as old-storage 401" for "scored as 409-but-penalised" — still under-
crediting the 111 dwellings, and pairing 409 with manual control is not a
combination RdSAP produces.
**HHRSH has a single, intrinsic charge control (2404).** The control is therefore
*named by the archetype*, not *unobserved* — unlike a generic storage heater whose
control genuinely is unknown. So ADR-0041's manual-default rule does not apply to
409.
## Decision
Add the baseline HHRSH archetype and let it drag its **coherent, intrinsic**
companions — mirroring how a gas-boiler archetype drags full modern controls
(`_control_for`) rather than a conservative default.
1. **`MainHeatingSystemType.ELECTRIC_STORAGE_HIGH_HEAT_RETENTION` =
`"Electric storage heaters, high heat retention"`**, mapped in
`_MAIN_HEATING_CODES` to **SAP Table 4a code 409**.
2. **Control 2404 (HHRSH controls), not the manual default.** `_control_for`
special-cases 409 → `2404` ahead of the generic storage → `2401`. HHRSH's single
intrinsic control is archetype-implied, an **exception to ADR-0041's
unobserved-storage manual default** (which stands for 401404).
3. **Electricity fuel (RdSAP 29).** `_natural_fuel_for` drags electricity for 409,
per ADR-0041's "storage → electricity" — so a mis-lodged non-electric fuel can't
leave an electric-storage dwelling incoherent. Scoped to 409 ("scoped per family
as archetypes land"); the pre-existing 401404 fuel-drag gap is not touched here.
4. **Dual/off-peak meter falls out of the code** — 409 is already in
`_ASSUMED_DUAL_METER_CODES`, so the off-peak meter drags automatically (ADR-0035
coherent companions).
5. **Deterministic guard + reclassify, TEXT-first / pgEnum-deferred.** A
`main_heating_guard` recognises the structured `"Electric Storage Systems: High
heat retention storage heaters"` phrasing (wired via `GuardedColumnClassifier`);
`scripts/reclassify_hhrsh_storage.py` reuses the same guard as its decision
function, updates `property_overrides.override_value` (TEXT) unconditionally, and
defers the `landlord_main_heating_system_overrides.value` pgEnum-cache write for
the new value.
## Consequences
- The 111 HHRSH dwellings score as **409 + HHR control 2404 + off-peak meter +
electricity** — the efficient modern archetype — instead of the worst old
large-volume 401. Correct Rebaselining (ADR-0039).
- **FE-owned pgEnum addition (Dan):** one new `main_heating_system` value,
`"Electric storage heaters, high heat retention"` (memory
`main-heating-system-pgenum-is-fe-owned`). TEXT read path is fixed on merge; the
cache write defers until the migration lands.
- **Baseline vs measure stay distinct but consistent:** both use code 409 +
control 2404; the measure additionally installs a designed HW cylinder / immersion
end-state, which a baseline (existing-system) override does not.
- Extends [ADR-0041](0041-landlord-heating-classification-targets-a-complete-modellable-taxonomy.md)
(complete taxonomy; **refines** its manual-default amendment for the single-
control HHRSH case) and [ADR-0035](0035-coherent-heating-system-synthesis.md)
(companions drag from the code).
### Alternatives rejected
- **409 + manual control (2401), holding to ADR-0041's default.** Rejected: HHRSH's
control is intrinsic (single option), not unobserved; the +0.7 °C manual penalty
under-credits, and 409+manual is not a real RdSAP combination.
- **Reuse an existing storage archetype (401404).** Rejected: none carries the
HHR retention/control — the whole point of the mis-score.
- **Leave fuel unresolved (as 401404 do).** Rejected for 409: an explicit
electricity drag keeps the system coherent if the lodged fuel is wrong; the older
archetypes' gap is a separate, out-of-scope cleanup.

View file

@ -0,0 +1,96 @@
# The "Gas CPSU" dumping ground splits into solid-fuel room-heater and electric-boiler archetypes
## Status
accepted
## Context
ADR-0041 named **"Gas CPSU" (SAP Table 4a 120, mains gas)** as the original
garbage-drawer archetype the LLM classifier over-used when the heating taxonomy
lacked a correct target. An audit of every `main_heating_system` override still
resolving to `"Gas CPSU"` found it standing in for three *different*, unrelated
systems — a mains-gas CPSU is neither solid fuel nor electric, so every one of
these is mis-scored (wrong system **and**, for the electric ones, wrong fuel):
| description | rows | true system |
| --- | --- | --- |
| Solid fuel room heaters: Open fire in grate | 16 | open fire (solid) |
| Solid fuel room heaters: Open fire with back boiler (no radiators) | 19 | open fire + back boiler |
| Solid fuel room heaters: Closed room heater with boiler (no radiators) | 2 | closed room heater + boiler |
| Boiler: A rated NA | 58 | **electric** boiler |
| Boiler: A rated CPSU | 1 | electric CPSU |
| Boiler: C rated CPSU | 1 | *genuine* gas CPSU — correct |
**The electric boilers are the sharpest mis-score.** All 58 `"Boiler: A rated NA"`
rows carry a `main_fuel = electricity` override — "NA" is the Hyde boiler-type
value meaning "not a gas combi / regular / CPSU", i.e. an **electric boiler**.
Classified as Gas CPSU they are scored as a mains-gas stored-water unit. A
lodged-cert check showed **defer-to-lodged is not viable**: 34 of the 58 lodge
**no** main-heating code at all (fuel 0), 11 lodge electric *room heaters* (691),
a few lodge community heating — the very gap the landlord override exists to fill.
So the faithful move is to model the electric boiler the landlord names, informed
by the electricity fuel we hold, not to defer to an empty cert.
The correct SAP Table 4a codes are exact (confirmed against
`domain/sap10_ml/sap_efficiencies.py`): **631** open fire (32%), **632** open fire
+ back boiler (50%), **634** closed room heater + boiler (65%); **191**
direct-acting electric boiler (100%). All are already scored by the calculator,
and 631/632/634 already sit in the overlay's `_SOLID_FUEL_ROOM_HEATER_CODES` set,
so their coherent companions (house-coal fuel 33, room-heater control 2601,
category 10) drag automatically (ADR-0035).
## Decision
Add the missing archetypes and reclassify the bucket. No calculator change.
1. **Three solid-fuel room-heater archetypes** in `_MAIN_HEATING_CODES`:
`"Solid fuel room heater, open fire"` → 631,
`"Solid fuel room heater, open fire with back boiler"` → 632,
`"Solid fuel room heater, closed with boiler"` → 634. Companions drag from the
code (already-covered 631636 set) — no overlay-logic change.
2. **`"Electric boiler"` → SAP 191** and **`"Electric CPSU"` → SAP 192**, both
dragging **electricity (29)** via `_natural_fuel_for` — electric boilers are
unambiguously electric (ADR-0041 "direct-acting/storage → electricity"), also
closing the pre-existing gap where 191/192 dragged no fuel. 191 stays on a
Single meter; 192 (electric CPSU) is already in `_ASSUMED_DUAL_METER_CODES`, so
it drags an off-peak Dual meter (§12 Rule 1, 10-hour).
3. **Deterministic `main_heating_guard` for the description-determined cases +
reclassify**, TEXT-first / pgEnum-deferred. The guard resolves the structured
phrasings by description alone: `"…open fire … back boiler"` → 632 (tested
before the bare `"open fire"` → 631), `"…closed … with boiler"` → 634, and
`"Boiler: <band> NA"``"Electric boiler"` (the NA type is the electric
signal). `scripts/reclassify_main_heating.py` reuses the guard as its decision
function.
4. **Electric CPSU is fuel-determined, not description-determined.** `"Boiler: A/C
rated CPSU"` is identical bar the efficiency band, so the electric/gas split
cannot be read from the description — the guard cannot see it. The reclassify
therefore resolves it with a **fuel-aware, per-property** step (`electric_cpsu_
for(description, main_fuel)`): a CPSU boiler whose property carries a
`main_fuel = electricity` override → `"Electric CPSU"` (192); a gas / unknown
fuel stays Gas CPSU (120). Only the **TEXT** layer is updated for it — the
description-keyed classifier cache cannot represent a fuel-dependent value. The
**live** guard stays fuel-blind, so a new electric-CPSU intake (rare — 1 row in
the corpus) falls to the LLM rather than being silently mis-guarded.
## Consequences
- 96 rows leave the Gas CPSU garbage drawer for faithful archetypes: 37 solid
fuel scored as solid fuel (not mains gas), 58 electric boilers + 1 electric CPSU
scored as electric (not a mains-gas CPSU); the 1 genuine gas CPSU stays on 120.
Correct Rebaselining (ADR-0039).
- **FE-owned pgEnum additions (Dan):** five new `main_heating_system` values —
`"Solid fuel room heater, open fire"`, `"Solid fuel room heater, open fire with
back boiler"`, `"Solid fuel room heater, closed with boiler"`, `"Electric
boiler"`, `"Electric CPSU"`. TEXT read path fixed on merge; cache writes defer to
the migration.
- Extends [ADR-0041](0041-landlord-heating-classification-targets-a-complete-modellable-taxonomy.md)
(complete taxonomy — retiring the Gas CPSU garbage drawer it named) and
[ADR-0035](0035-coherent-heating-system-synthesis.md) (companions drag from the
code).
### Out of scope
- **Whether "NA" ever denotes a non-electric boiler.** It is 100% electric in the
corpus and the guard treats it as electric; a future non-electric "NA" would be
a re-open signal, not a silent mis-map (the fuel override would disagree).

View file

@ -0,0 +1,92 @@
# Electric underfloor is scored by its own SAP codes; the meter defers to the cert when the tariff is ambiguous
## Status
accepted
## Context
Electric-underfloor `main_heating_system` overrides had no archetype, so the LLM
classifier funnelled them into the nearest wrong bucket (#1376):
| description | rows | mis-mapped to |
| --- | --- | --- |
| Electric Underfloor Heating: In concrete slab (off-peak only) | 15 | Direct-acting electric (191), **Single** meter |
| Electric Underfloor Heating: In screed above insulation (standard or off peak) | 63 | Direct-acting electric (191), **Single** meter |
| Electric Underfloor Heating: Integrated (storage+direct-acting) (off peak) | 12 | Electric storage heaters, old (401) |
The **meter is the crux**. A Landlord heating override drags an assumed meter from
its SAP code (ADR-0035), and the `keep_existing_off_peak_meter` mitigation
([overlay_applicator.py](../../domain/modelling/scoring/overlay_applicator.py))
protects a cert's lodged off-peak meter — but *only when the overlay's own desired
meter is off-peak*. Direct-acting (191) is a **Single**-meter system, so the
mitigation never fires and the overlay **forces Single**, overwriting the cert's
meter. For off-peak underfloor that bills overnight heat at the peak rate and
craters SAP — the exact Class-A meter defect.
SAP Table 4a has **dedicated electric-underfloor codes** (labels from
`cert_to_inputs.py`): **421** in concrete slab (off-peak only), **422** integrated
(storage+direct-acting), **424** in screed above insulation (423 integrated-low-
off-peak and 425 timber-floor are not in the corpus). Per SAP 10.2 §12, **421/422
are off-peak (Rule 2, 7-hour → Dual)**; **424 is not off-peak-implying (Single)**.
A lodged-meter audit is decisive for the screed case: **51 of 63 (81%)** of the
`"…in screed (standard or off peak)"` dwellings lodge an **off-peak** meter (24-hour
dual or 7/10-hour), the rest single. The description itself says "standard **or**
off peak" — so the archetype genuinely cannot know the tariff; the cert does.
Forcing 424/Single would downgrade 81%; forcing Dual would over-credit the ~19% on
single.
## Decision
Add three electric-underfloor archetypes mapping to their own SAP codes, and treat
the meter per what the description actually asserts.
1. **New archetypes** in `_MAIN_HEATING_CODES`:
`"Electric underfloor, in concrete slab (off-peak)"`**421**,
`"Electric underfloor, integrated storage and direct-acting"`**422**,
`"Electric underfloor, in screed above insulation"`**424**.
2. **Electricity fuel (29)** drags for 421/422/424 via `_natural_fuel_for` — electric
underfloor is unambiguously electric (ADR-0041), closing the prior no-fuel gap.
3. **421 and 422 assert an off-peak Dual meter.** Their descriptions positively
claim off-peak ("off-peak only" / "off peak"), and both are already in
`_ASSUMED_DUAL_METER_CODES`; the `keep_existing_off_peak_meter` mitigation keeps
a more-specific cert off-peak meter and applies Dual to a single-rate cert.
4. **424 (screed) defers the meter to the cert.** Because "standard or off peak" is
tariff-agnostic and 81% lodge off-peak, `_meter_for` returns **`None`** for 424
(a new meter-agnostic set), so the overlay leaves `meter_type` unset and the
lodged meter stands — each dwelling scores on its actual tariff. This is a
**targeted exception to the "always set the meter, never let it bleed"
invariant**: that invariant guards against a switched-*off* storage system
leaving a stale Dual meter, but an underfloor override *confirms* underfloor —
there is no switch-off, so deferring is faithful, not a bleed.
5. **Deterministic `main_heating_guard` + reclassify** (TEXT-first, pgEnum-deferred),
reusing the generalized `reclassify_main_heating.py`. All three underfloor
descriptions are description-determined, so the guard resolves them directly (no
fuel-awareness needed) and the reclassify follows.
## Consequences
- The 15 concrete-slab-off-peak rows stop being billed at the peak rate (191/Single
→ 421/Dual); the 12 integrated rows get the correct underfloor code (401 → 422,
meter already Dual); the 63 screed rows keep their lodged (81% off-peak) meter and
score on code 424 with correct screed responsiveness (Table 4d R=0.75). Correct
Rebaselining (ADR-0039).
- **FE-owned pgEnum additions (Dan):** three new `main_heating_system` values —
`"Electric underfloor, in concrete slab (off-peak)"`, `"Electric underfloor,
integrated storage and direct-acting"`, `"Electric underfloor, in screed above
insulation"`. TEXT read path fixed on merge; cache writes defer to the migration.
- `_meter_for` becomes `Optional[str]` (a code may defer the meter). Every existing
code keeps its current Single/Dual answer; only 424 returns `None`.
- Extends [ADR-0041](0041-landlord-heating-classification-targets-a-complete-modellable-taxonomy.md)
and [ADR-0035](0035-coherent-heating-system-synthesis.md); refines the
`keep_existing_off_peak_meter` behaviour ADR-0035 introduced.
### Alternatives rejected
- **Force Single (424 per spec).** Rejected: downgrades the 81% lodged off-peak,
reproducing the meter bug.
- **Force Dual for screed.** Rejected: over-credits the ~19% genuinely on single and
asserts a tariff the "standard or off peak" description does not claim.
- **Map underfloor to Direct-acting (191) / old storage (401) as today.** Rejected:
wrong code (responsiveness/meter) and the source of the mis-score.

View file

@ -0,0 +1,119 @@
# Roof Eligibility Resolves Sentinel Thickness by Age Band (vaulted joins sloping; ND ≠ NI)
Property 742265 (uprn 10033526327, cert 2780-3922-3202-6042-9200) lodges a
`Pitched (vaulted ceiling)` roof (gov-API `roof_construction` 5) with
`roof_insulation_thickness = "ND"` and got **no roof Recommendation**, leaving
its plan short of the scenario's goal band on an unlimited budget. Two
generator gaps compound (ADR-0021's dispatch table has no vaulted row, and its
uninsulated trigger `!= 0` compares the `"ND"` *string* against int 0, so it
never fires), while the SAP10 Calculation already handles both: it treats
vaulted as a slope-following ceiling and resolves the `"ND"` sentinel via the
roof description / age-band defaults (`heat_transmission.py` — the
`is_sloping_ceiling` disjunction and the raw-sentinel inspection). The
generator diverged from the calculator — the exact split ADR-0021 set out to
prevent.
## Decision
**Amends ADR-0021.** The roof Recommendation Generator changes in two ways;
the mapper and the calculator change in none.
**1. Vaulted is a sloping ceiling.** The dispatch matches the same substring
disjunction the calculator uses (`"sloping ceiling" in roof_type or "vaulted"
in roof_type`): a vaulted ceiling has no loft void, so its one applicable
Measure is `sloping_ceiling_insulation` (rafters, 100 mm) — never the loft
fallback. ADR-0021's dispatch table gains the row:
| Roof type (`roof_construction_type` substring) | Measure | Uninsulated trigger | Overlay |
|---|---|---|---|
| `sloping ceiling` **or `vaulted`** | `sloping_ceiling_insulation` | resolved thickness 0 (below) | → 100 mm |
**2. Sentinel-aware uninsulated trigger, resolved by age band — codes, not
descriptions.** `roof_insulation_thickness` is `Optional[Union[str, int]]`; a
lodged string sentinel is data, not noise. The generator resolves eligibility
for the two pitched branches (sloping/vaulted and loft) as:
| Lodged value | Meaning (RdSAP 10) | Eligibility |
|---|---|---|
| int `0` | explicitly uninsulated | eligible (unchanged) |
| int `> 0` | measured thickness | not eligible (unchanged) |
| `"NI"` | insulation **present**, thickness unknown (§5.11.4: assume 50 mm) | **not eligible** |
| `"ND"` / `None` | not defined — resolve to the age band's **as-built** state | eligible iff as-built uninsulated (bands **AD**) |
Age-band as-built states (construction age code): **AD uninsulated · EF
limited insulation · G+ insulated.** Only AD resolves to eligible. This
extends the codebase's existing precedent (`_PRE_1950_AGE_CODES = {A,B,C,D}`
in the mapper's code-8 rule; pinned by cert 001479 Ext2 at U=2.30) from the
scoring path to eligibility.
**Sentinel resolution requires a KNOWN pitched/thatched roof type.** An
unlodged `roof_construction_type` reaching the loft fallback can be a **party
ceiling** — gov-API codes 6/7 ("(another dwelling above)" / thatch with
additional insulation) map the type to `None`, and real code-7 certs lodge
`"ND"` — with zero roof heat loss (RdSAP 10 Table 18), so insulating it
contradicts the dwelling. An unlodged type therefore keeps the explicit-0-only
trigger (the pre-ADR behaviour for that shape); only a type containing
`pitched`/`thatch` (or the sloping/vaulted branch, known by construction)
resolves sentinels by age band.
**Coherence rule (load-bearing):** sentinel resolution must never make an
undefined cert *more* eligible than an explicitly-lodged one. An explicit
50 mm lodgement is not eligible today, so `"ND"` on a band-E/F ("limited
insulation") dwelling must also resolve to not-eligible — recommending a
top-up to the dwelling whose insulation we *don't* know while withholding it
from the identical dwelling whose thin insulation we *do* know would be
incoherent. Whether limited-insulation **top-up** should be a Measure at all
is a separate product question that applies equally to explicit thin
lodgements; it is out of scope here (see Consequences).
**The mapper stays untouched — the `"ND"` passthrough is load-bearing.** The
calculator inspects the *raw* lodgement to distinguish explicit `int(0)`
(drop the description; uninsulated) from `"NI"` (keep the description; §5.11.4
retrofit-50 mm) from `"ND"` (defer to description / age default). Normalising
sentinels at the mapper boundary would change scoring inputs for every ND-roof
cert in the corpus for zero eligibility benefit.
**Two homes for sentinel resolution, deliberately.** Scoring asks "what
thickness should the U-cascade see" (`NI` → 50 mm); eligibility asks "is this
roof a candidate" (`NI` → no). Same sentinel, different questions, different
answers — a single shared resolver would just relocate the split into its call
sites. Each home carries a regression test pinning the *disagreement* as
intentional. Consolidate only if a third consumer appears (rule of three).
## Considered options
- **Normalise `ND` → 0 in the mapper** (extend the code-8 rule to code 5 and
to the `"ND"` string). Rejected: changes calculator inputs corpus-wide —
`roof_thickness_explicitly_zero` would fire and drop the roof description
the §5.11 resolution depends on — risking the pinned Elmhurst accuracy
corpus to fix an eligibility bug.
- **Defer to `roofs[].description`** ("no insulation (assumed)") for ND
resolution, mirroring the calculator exactly. Rejected: eligibility should
key on codes, not free-text description parsing; descriptions drift by
lodgement software and the age-band code carries the same as-built signal
deterministically.
- **Gate eligibility on the calculator's effective roof U-value.** Rejected:
no other generator gates on scored output (walls/floors gate on lodged
attributes), and it re-couples eligibility to scoring against ADR-0016's
separation.
- **Treat `"ND"` alone as uninsulated** (sentinel-literal). Rejected: ND means
*not defined*, not *none* — a band-G ND roof is almost certainly insulated
as built and would get a junk Recommendation.
## Consequences
- 742265 (band B, vaulted, ND) becomes eligible: `sloping_ceiling_insulation`
over 55.82 m² (~£2.2k, verified by local re-model). Any pitched-loft cert
lodging `"ND"` with age band AD is likewise un-withheld.
- The **flat branch is deliberately untouched**: its inverted convention
(`None` = uninsulated, per the Elmhurst mapping) means an `"ND"` flat roof
is *not eligible* today and stays so — a separately-verifiable follow-up,
not silently folded into this change.
- **Open follow-up (product):** should limited-insulation dwellings (explicit
thin lodgements *and* ND + bands EF alike) be offered an insulation
top-up? Today neither is; changing that is a trigger redesign, not a
sentinel question.
- ADR-0021's "one dispatching generator, one Measure per roof" and its
dispatch order (sloping → flat → no-access → loft) are unchanged; the
sloping test simply matches one more substring, ahead of the fallbacks as
before.

View file

@ -0,0 +1,132 @@
# A system-replacing heating override forces category and control for coherence, and defers fuel and meter where multiple values are valid
## Status
accepted
## Context
A **Landlord Override** naming a main heating system resolves to a SAP Table 4a
code, and `main_heating_system_overlay` synthesises the **coherent companions**
that code implies (ADR-0035): heating category, charge control, natural fuel,
meter. The overlay is applied over the lodged cert by last-wins composition, so a
companion the overlay leaves **unset (`None`)** is *inherited from the replaced
system* — not cleared.
That inheritance is a silent mis-rating when the archetype genuinely determines
the companion. A portfolio-796 audit found the case end-to-end on **86+
properties**: a lodged **slimline storage heater** (SAP 402, category 7, control
2401, off-peak) overridden to **"Electric room heaters"** (SAP 691). The overlay
stamped code 691, fuel, and a Dual meter — but `_category_for(691)` and
`_control_for(691)` both returned `None`, because their room-heater branch keyed
on `_CATEGORY_10_ROOM_HEATER_CODES`, a set that held only the *fuel-burning*
(gas/oil/solid) room heaters and **excluded electric 691701** despite electric
room heaters also being Table 4a **Category 10**.
Two independent mis-ratings followed from the surviving storage companions:
1. **Category 7 survived.** The SAP 10.2 Table 12a resolver keys on category
first, read `OTHER_STORAGE_HEATERS`, and — with the dwelling on the §12 Rule 3
10-hour tariff — hit `(OTHER_STORAGE_HEATERS, TEN_HOUR)`, a combination the
Grid-1 table does not carry, so it fell to the all-low-rate fallback. The
peaky room heaters were billed **100% at the night rate** — the precise
"all-low over-credit" the overlay's own Dual-meter comment claims to avoid.
Effect: baseline SAP over-rated **64.1 vs the spec-correct 56.4**; the HHRSH
upgrade measure showed a **negative** bill saving (£13/yr) because the
baseline it was compared against was itself under-billed.
2. **Control 2401 survived.** `main_heating_control` fed Table 4e as the storage
**manual charge control → +0.7 °C** mean-internal-temperature adjustment — the
largest in the table — riding on a direct-acting room heater, which should
carry the Group 6 room-heater control **2601 → +0.3 °C**.
The companions are **not uniform** in how strongly the archetype determines
them, which is the crux of the decision:
- **Category is deterministic** — SAP Table 4a assigns exactly one category per
code (gas→2, heat pump→4, network→6, storage→7, **electric underfloor→8**,
warm-air→9, **room heater→10**). A `None` category is never valid.
- **Control is an assumption but always archetype-supplied** — the Table 4e
*Group* is fixed by category; the specific conservative default within the
group is a modelling choice (storage→manual 2401, HHRSH→2404 per
[ADR-0044](0044-hhrsh-baseline-archetype-drags-its-intrinsic-controls.md),
room heater→2601, gas boiler→full controls). It is never "inherit the replaced
system".
- **Fuel is sometimes genuinely ambiguous** — electric/gas/storage/heat-pump
archetypes pin their fuel, but **community heating** could be gas CHP, biomass,
or waste heat, so its fuel legitimately **defers** to a `main_fuel` override,
else the lodged value (ADR-0041 last-wins).
- **Meter is physics-locked for some systems, flexible for others** — storage /
CPSU / HHRSH **require** Dual (they charge overnight); non-electric systems are
stamped Single to stop an Economy-7 split bleeding onto a gas boiler
([ADR-0035](0035-coherent-heating-system-synthesis.md)); screed underfloor
defers to the cert ([ADR-0046](0046-electric-underfloor-scored-by-its-own-codes-meter-deferred-when-tariff-ambiguous.md)).
## Decision
A system-replacing heating override must emit a **complete, coherent companion
set**. Companions split by whether the archetype *determines* the value:
1. **Category and control are archetype-forced** — always written, never
inherited, because a wrong value mis-scores or mis-bills the cert. Electric
room heaters (Table 4a 691701) join the Category-10 room-heater branch of
both `_category_for` (→ category **10**) and `_control_for` (→ control
**2601**, the Group 6 conservative no-thermostat default, matching the
gas/oil/solid siblings). Their fuel (electricity 29) and Dual meter were
already correct and are unchanged.
2. **Fuel and meter defer where multiple values are coherent** — this is *not*
a gap. Fuel defers only for genuinely ambiguous carriers (community heating);
meter defers only for meter-flexible systems (room heaters run coherently on
either meter; screed underfloor per ADR-0046). Meter-locked systems still
force (storage/CPSU/HHRSH → Dual; non-electric → Single).
3. **An incomplete companion set is logged, not raised (for now).** Where the
overlay cannot yet supply a confident category or control — heat pumps
(Group 2), community (Group 3), electric underfloor (Group 7), electric
boiler / CPSU (Group 1) — `main_heating_overlay_for` **logs an error** naming
the archetype and code and continues, matching the `flag_fuel_mismatch`
log-not-raise convention. Log-and-continue keeps a mid-portfolio run alive
and surfaces the gap (CloudWatch) for review, rather than turning a latent
inheritance bug into an outage. The intended end-state is to fill each
archetype's category/control with spec sight and then **flip the log to a
raise**, so the coherence invariant is enforced.
Category expansion for the still-`None` archetypes (electric boiler/CPSU→2,
underfloor→8) is deterministic from Table 4a but **deferred** to its own change:
it flows into the category-keyed Table 12a resolver and needs a corpus check, so
it does not ride on the room-heater fix.
## Consequences
- The 86+ room-heater dwellings score against a correctly-billed baseline: the
space-heating high-rate fraction is the spec **0.50** (10-hour direct-acting),
not 0.0; baseline SAP corrects **64.1 → 56.4**; the HHRSH measure's bill saving
flips **£13/yr → +£235/yr** (property 710788, uprn 22003310, scenario 1268).
- The `refetch_solar`-corrected portfolio's `negative-bill-savings` audit group
loses its ~86-property HHRSH-on-room-heater cluster after re-model.
- The overlay now emits a visible error for any archetype it cannot fully
companion — a standing signal (wire a CloudWatch metric filter on the log
string) for which control default to fill next.
- Extends [ADR-0035](0035-coherent-heating-system-synthesis.md) (companions drag
from the code) and [ADR-0041](0041-landlord-heating-classification-targets-a-complete-modellable-taxonomy.md)
(complete taxonomy); sits beside
[ADR-0044](0044-hhrsh-baseline-archetype-drags-its-intrinsic-controls.md)
(a control named by the archetype) and
[ADR-0046](0046-electric-underfloor-scored-by-its-own-codes-meter-deferred-when-tariff-ambiguous.md)
(meter deferred where the tariff is ambiguous).
### Alternatives rejected
- **Make every companion total (force fuel and meter too).** Rejected: it breaks
the deliberate deferrals — community-heating fuel (ambiguous carrier) and the
ADR-0046 screed-underfloor meter — whose `None` is a load-bearing "defer to the
cert/override" signal, not a gap.
- **Fix the Table 12a resolver to prefer the SAP code over a stale category.**
A sound defence-in-depth follow-up, but it treats the symptom; the overlay
emitting an incoherent cert is the root cause, and category/control coherence
must hold for every consumer (predicted EPCs, raw lodged certs), not only the
billing resolver.
- **Raise on an incomplete companion set now.** Rejected for this change: heat
pump / community / underfloor overrides model today (by luck of inheritance),
so an immediate raise is a blast-radius change mid-campaign. Log now, raise once
the common archetypes are filled.

View file

@ -0,0 +1,98 @@
# The ASHP recommendation sizes the heat pump to the dwelling's design heat loss
## Status
accepted
## Context
The ASHP measure (ADR-0024) installed **one fixed heat pump** — a 4.37 kW Vaillant
aroTHERM plus (PCDB Table 362 record 110257) — on every eligible dwelling, as a
"representative, contractor-installable" end-state. SAP 10.2 Appendix N reads a
heat pump's efficiency at the dwelling's **PSR** (Power-to-Space-heating-Ratio =
rated output / design heat loss): efficiency peaks around PSR 0.8 and, per
footnote 44, **collapses toward 100% below the record's smallest PSR**.
A portfolio-796 audit found the fixed pump scoring the ASHP **below the baseline**
on high-heat-loss dwellings. Property 712794 (uprn 10002468116): a leaky dwelling
with a **17.5 kW design heat loss**, so a 4.37 kW pump lands at **PSR 0.25** — the
efficiency collapse zone. The calculator correctly applied ~195% SPF; combined
with pricing electricity at the standard tariff against an off-peak storage
baseline, the ASHP scored **SAP 10.33 vs the baseline 11.96 (1.63)** and the
Optimiser never selected it. Every electric-heated dwelling too leaky for a 4.37 kW
pump was capped below the goal band by this — a modelling defect, not a calculator
one (SAP's PSR curve is correct: a grossly undersized heat pump *is* inefficient).
The fix needs the dwelling's design heat loss, which the calculator computes
internally (annual-average HLC × the SAP design temperature difference, 24.2 K)
but did not surface.
Sizing raised a second question the accredited data answered. MCS capacity sizing
targets **PSR ≈ 1.0** (output ≥ design heat loss). But the **relodged Elmhurst ASHP
cert** — a real dwelling our calculator gives a 6.40 kW design heat loss — was
fitted with the **5 kW** Vaillant (110257, 4.37 kW output), i.e. **PSR 0.68**, and
scores SAP 72.08. A PSR-1.0 rule picks the 7 kW pump for that dwelling → SAP 73.86,
**oversizing by a rung and inflating SAP by ~1.8**, and no longer reproduces the
accredited cert. SAP efficiency at PSR 0.68 and 1.0 is near-identical (~400%), so
the real installer's smaller, cheaper choice is the realistic one.
## Decision
Size the heat pump to the dwelling and expose what sizing needs:
1. **`SapResult.design_heat_loss_kw`** — the calculator surfaces the design heat
loss (annual-average HLC × 24.2 K / 1000), the quantity Appendix N's PSR
divides the pump output by.
2. **A curated aroTHERM plus ladder** — one representative PCDB record per nominal
size across the domestic range (`_ASHP_SIZING_LADDER`): 3.5 kW (110249), 5 kW
(110257, the prior anchor), 7 kW (110265), 10 kW (110273), 12 kW (110281). All
the same "& AI VIH RW" variant, so the ladder is a consistent product line.
3. **Size to the Appendix-N efficiency peak, not MCS capacity.**
`select_ashp_pcdb_id` picks the rung whose rated output is nearest **0.8 ×
design heat loss** (PSR ≈ 0.8, the efficiency peak). This reproduces the pump a
real installer fits — validated against the relodged Elmhurst cert at delta 0 —
and keeps the pump clear of the low-PSR collapse. A load beyond the 12 kW range
naturally selects the largest rung (an honest, capped undersize; those
dwellings need fabric-first).
4. **Cost and efficiency share one figure.** `ashp_cost_inputs` takes the same
design heat loss (over the floor-area proxy), so the rate-sheet install band
(ADR-0025) and the pump's efficiency record are sized from one number. The
orchestrator reads `design_heat_loss_kw` off a baseline score and threads it
into the heating generator; the harness computes it via a calculator pass.
## Consequences
- Property 712794's ASHP goes from **1.63 SAP (never selected)** to **+44.2 SAP
(selected)**; its plan lifts **G (12.0) → B (89.1)**. Electric-heated dwellings
previously capped below goal C by the undersized pump can now reach it — the
largest HIGH-severity mover on the portfolio-796 `plan-stops-short-of-goal` and
`plan-stuck-in-low-band` groups.
- The accredited relodged Elmhurst ASHP cert still reproduces at **delta 0** (the
6.40 kW dwelling sizes to the 5 kW Vaillant it was actually fitted with). Two
self-snapshot cascade pins re-pin to their correctly-sized larger pumps (both
dwellings were undersized by the fixed unit, so their end-state SAP rises).
- Adding `design_heat_loss_kw` to `SapResult` is a required field; manual
constructions (test stubs) carry it.
- Extends [ADR-0024](0024-ashp-bundle-absolute-end-state.md) (the ASHP end-state)
and [ADR-0025](0025-ashp-cost-composed-from-the-rate-sheet.md) (size-banded
cost); the cost bands already scaled — they now receive the real heat loss.
### Alternatives rejected
- **MCS capacity sizing (PSR ≈ 1.0, output ≥ design heat loss).** Rejected: it
oversizes relative to real installs, inflating SAP ~1.8 per dwelling and
breaking the accredited-cert reproduction. SAP efficiency is equivalent at
PSR 0.681.0, so the realistic (installer) size is preferred.
- **Keep the fixed 4.37 kW pump.** Rejected: the collapse on high-heat-loss
dwellings is the defect under repair.
- **Fabric-first sequencing (score the ASHP only after insulation).** A real
concern — the Optimiser scores each measure independently against the
un-insulated baseline (ADR-0016) — but a separate change to measure scoring/
sequencing, not pump sizing. Sizing the pump fixes the standalone score
regardless; noted for follow-up.
- **Nearest record from all ~10,000 Table 362 heat pumps.** Rejected: an opaque,
unauditable selection; a curated single-product ladder keeps ADR-0024's
"representative, contractor-installable" intent.

View file

@ -0,0 +1,84 @@
# An electric-underfloor override drags category 8 and the conservative Group 7 control
## Status
accepted
## Context
[ADR-0048](0048-heating-override-forces-category-and-control-defers-fuel-and-meter.md)
established that a system-replacing heating override must emit a **complete,
coherent companion set** — category and control are archetype-forced, never
inherited — and explicitly deferred the category expansion for the still-`None`
archetypes ("electric boiler/CPSU→2, underfloor→8 … flows into the
category-keyed Table 12a resolver and needs a corpus check") to its own change.
The portfolio-796 audit (PRD #1435, WS2) found the underfloor half of that gap
end-to-end on property **711795** (uprn 10090343115): a lodged **community
heating** cert (SAP 301, category 6, control 2311) overridden to *"Electric
underfloor, in screed above insulation"* (SAP 424,
[ADR-0046](0046-electric-underfloor-scored-by-its-own-codes-meter-deferred-when-tariff-ambiguous.md)).
`_category_for(424)` and `_control_for(424)` both returned `None`, so the
effective cert kept the replaced heat network's **category 6** and community
control. Two independent mis-ratings followed:
1. **No heating pathway.** `is_heat_network_main` keys on category 6, so the
heating Recommendation Generator excluded both HHRSH and ASHP by the
heat-network topology rule — `recommend_heating` returned `None` and the
flat's whole candidate menu was a no-op wall measure plus lighting. The
dwelling was unliftable from band E (the `plan-stuck-in-low-band` audit
group counts ~7 electric-underfloor dwellings).
2. **Baseline mis-rated.** The calculator scored the electric underfloor as
community heating: baseline SAP **51 (E)** vs **67 (D)** with coherent
companions. With the pathway restored, HHRSH scores **80 (C)** for ~£1k —
the dwelling reaches the scenario goal.
## Decision
The three electric-underfloor archetypes (SAP Table 4a **421 / 422 / 424**,
ADR-0046) join the forced-companion branches of `main_heating_system_overlay`:
1. **Category 8** ("Electric underfloor heating") in `_category_for`
deterministic from SAP Table 4a, per ADR-0048's category rule.
2. **Control 2701** (SAP Table 4e **Group 7**, "no time or thermostatic control
of room temperature", **+0.3 °C**) in `_control_for` — the conservative
default within the group the category fixes: the largest mean-internal-
temperature adjustment in Group 7, so an unobserved control is never
over-credited. Mirrors the storage manual-charge 2401
([ADR-0035](0035-coherent-heating-system-synthesis.md)) and room-heater 2601
(ADR-0048) precedents.
Fuel (electricity 29) and meter (Dual for 421/422; **deferred to the cert** for
tariff-ambiguous 424) are already correct per ADR-0046 and are unchanged.
**Scope**: underfloor only. Electric boiler / CPSU (Group 1) remain
log-and-continue gaps tracked by #1444 — their category (2) feeds the wet-boiler
fallback classification in the generators and needs its own check.
## Consequences
- An underfloor override on any lodged system now yields a coherent cert: the
Table 12a resolver and the heat-network gates read the dwelling as electric
underfloor, the baseline re-rates correctly, and the HHRSH bundle is offered
where eligible (a flat's only heating pathway — ASHP stays house/bungalow-only
per ADR-0024).
- The "incomplete companion set" error log no longer fires for 421/422/424.
- **Known deferred gap (pre-existing, unchanged by this ADR):** the Table 12a
space-heating resolver's underfloor rows are wired in the fraction table but
`_table_12a_system_for_main` never returns `UNDERFLOOR_HEATING` ("Underfloor
heating (421-422) — TODO"), so an off-peak-metered underfloor dwelling falls
to the all-low-rate fallback. That affects genuinely-lodged underfloor certs
identically and needs spec sight (SAP 10.2 Table 12a Grid 1, PDF p.191) to
split slab (storage-like) from screed (direct-acting-like); tracked as a
follow-up, not ridden on this override fix.
### Alternatives rejected
- **A zero-adjustment Group 7 control (2703/2704).** Rejected: credits a room
thermostat the landlord never reported; the conservative default is the
ADR-0035/0048 pattern.
- **Completing electric boiler / CPSU (category 2) in the same change.**
Rejected: no evidence property in this workstream, and category 2 flows into
the generators' wet-boiler fallback (`_WET_BOILER_CATEGORIES`), which would
make an electric-boiler override newly eligible for tune-up options — a
behaviour change needing its own corpus check (#1444).

View file

@ -0,0 +1,76 @@
# Wall insulation is offered only when it lowers the derived wall U-value
## Status
accepted
## Context
The wall Recommendation Generators trigger on the wall's **insulation state**
alone: `recommend_cavity_wall` offers cavity fill for any cavity wall whose
`wall_insulation_type` is as-built (4), and `recommend_solid_wall` offers
EWI/IWI for any as-built suitable construction
([ADR-0019](0019-solid-wall-insulation-options.md)). Neither asks whether
adding insulation can actually **change the modelled wall**.
For modern construction age bands it cannot: RdSAP's Table 6-10 cascade
(`u_wall` in `domain/sap10_ml/rdsap_uvalues.py`) assigns an as-built wall its
age band's at-regs U-value, and the post-measure state resolves to the **same
value**. Portfolio-796 evidence (PRD #1435, WS2): property **711795**, timber
frame at age band **L** (20122022) — `u_wall` returns **0.280 W/m²K both
as-built and with 100 mm IWI**, so the persisted plan offered
`internal_wall_insulation` at **£4,027 for +0.00 SAP**. Any modern-age-band
dwelling gets the same no-op fabric offer; on an unliftable dwelling it is the
*entire* remedial menu, which reads as "we recommend spending £4k for nothing".
Scoring already prices the option at +0.00, but the Optimiser keeps
zero-uplift candidates in the persisted menu (they are legitimate for some
measure types, e.g. secondary-heating removal saves bills without SAP), so the
fix belongs at **generation eligibility**, not a blanket score filter.
## Decision
Both wall generators consult the **derived wall U-value** before offering: the
measure is offered only when the post-measure state's `u_wall` is **lower than
the current state's by more than 0.01 W/m²K** (a float-noise guard, not a
materiality threshold — economics stay with the Optimiser, ADR-0016/0024).
- **Current state**: `u_wall` with the effective main wall's construction, age
band, lodged insulation type/thickness — a thin parameter mapping in the
`envelope.py` style, deliberately omitting the calculator's rich extras
(surveyor description, dry-lining, documentary-thickness routes). Omissions
bias the gate toward **offering** (both sides are computed identically, so a
no-op still compares equal), never toward suppressing a real measure.
- **Post-measure state**: the same call with the Option's overlay applied —
filled cavity (`wall_insulation_type=2`) for cavity fill; 100 mm
internal/external insulation for IWI/EWI.
## Consequences
- Age-band-L (and any at-regs age band) walls stop polluting candidate menus
with £4k no-ops; genuinely treatable walls (e.g. age-band-D solid brick,
U 2.1 → ~0.3) are offered exactly as before.
- The generators gain a read on `domain/sap10_ml/rdsap_uvalues.u_wall` — same
precedent as the heating generator reading the calculator's PCDB/Table 4b.
- **Deferred:** (a) a cert that lodges an explicit `wall_u_value` overrides the
cascade in the calculator, making fabric measures score +0.00 regardless of
this gate — the measure overlay should adjust the lodged U (probed on
portfolio 796: 0/300 sampled certs lodge one, so no live evidence yet);
(b) roof/floor generators have the same anti-pattern in principle (roof
age-band eligibility is partly covered by ADR-0047). **Amended:** the floor
arm landed as `floor_u_value_gate` (probed live: solid_floor_insulation at
£3-4k for +0.00 on age-band-L dwellings 709782/709790); a 40-dwelling
band-K/L probe found no roof no-ops, so the roof arm stays unimplemented
until evidence appears.
### Alternatives rejected
- **Filter zero-SAP candidates after scoring.** Rejected: zero-uplift options
are legitimate for some measure types (bill-driven), and the pollution would
survive in any consumer that reads Generator output before scoring.
- **Hard-code "age band ≥ K ⇒ no wall offer".** Rejected: duplicates the U
tables coarsely; the tables already encode exactly which
construction × age-band × insulation combinations move.
- **Full-fidelity U parity with the calculator (description, dry-lining,
lodged U).** Rejected for now: heavier coupling for no observed benefit —
the omissions only ever bias toward offering, which is the status quo.

View file

@ -18,10 +18,10 @@ straight lift-and-shift of the columns below.
|---|---|---|
| `id` | serial PK | |
| `property_id` | int, FK → `property.id`, **unique** | one Baseline Performance per Property |
| `lodged_sap_score` | int | Lodged Performance — gov register, off the Effective EPC |
| `lodged_epc_band` | text | the `Epc` enum, stored as its string value (e.g. `"C"`) |
| `lodged_co2_emissions_t_per_yr` | float | tonnes CO₂/yr (whole dwelling) |
| `lodged_primary_energy_intensity_kwh_per_m2_yr` | int | PEUI (kWh/m²/yr); **not** "heat demand" — see CONTEXT.md |
| `lodged_sap_score` | int, **nullable** | Lodged Performance — gov register, off the Effective EPC. **NULL for a predicted Property** (no lodged cert — #1361, see below) |
| `lodged_epc_band` | text, **nullable** | the `Epc` enum, stored as its string value (e.g. `"C"`) |
| `lodged_co2_emissions_t_per_yr` | float, **nullable** | tonnes CO₂/yr (whole dwelling) |
| `lodged_primary_energy_intensity_kwh_per_m2_yr` | int, **nullable** | PEUI (kWh/m²/yr); **not** "heat demand" — see CONTEXT.md |
| `effective_sap_score` | int | Effective Performance — what modelling scored against |
| `effective_epc_band` | text | |
| `effective_co2_emissions_t_per_yr` | float | tonnes CO₂/yr (whole dwelling) |
@ -30,6 +30,21 @@ straight lift-and-shift of the columns below.
| `space_heating_kwh` | float | EPC `renewable_heat_incentive` recorded demand. **Superseded** by `heating_kwh` (delivered) when the bill block populates; kept until then to avoid an empty-kWh gap, dropped in the population slice. |
| `water_heating_kwh` | float | EPC `renewable_heat_incentive`; **superseded** by `hot_water_kwh`. |
### Lodged half is nullable (#1361 Class B, 2026-06-30)
The four `lodged_*` columns are **nullable**. A **predicted** Property (EPC Prediction, ADR-0029/0031:
no lodged cert, its `EpcPropertyData` synthesised from neighbours) has **no Lodged Performance** — the
backend now writes `lodged_* = NULL` for it and persists only the Effective half (ADR-0004 amendment).
Reading the synthesised EPC's recorded fields as a lodged figure had been manufacturing a phantom (a
neighbour's SAP), which the migration + a one-time backfill remove.
**FE-owned Drizzle migration required:** `ALTER TABLE property_baseline_performance ALTER COLUMN
lodged_sap_score DROP NOT NULL` (and the same for `lodged_epc_band`, `lodged_co2_emissions_t_per_yr`,
`lodged_primary_energy_intensity_kwh_per_m2_yr`). This **must land before** the backend deploys the
orchestrator change or runs `scripts/null_predicted_lodged_performance.py` — a `NULL` write against a
`NOT NULL` column aborts the batch (ADR-0012). The backfill NULLs the four columns on the ~12,236
existing predicted-source rows; Effective, the bill block, and `rebaseline_reason` are left intact.
### Bill block (ADR-0014) — the energy bill, composed per section
Produced by **Bill Derivation**: the calculator's **delivered** kWh per end use priced at current

0
domain/abri/__init__.py Normal file
View file

View file

@ -0,0 +1,18 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class JobDescriptions:
short_description: str
long_description: str
def build_job_descriptions(deal_name: str) -> JobDescriptions:
# Placeholder copy — final wording is owned by Abri's schedulers (issue #1455).
return JobDescriptions(
short_description=f"Domna condition survey - {deal_name}",
long_description=(
f"Domna condition survey visit at {deal_name}, "
"booked by Domna operations."
),
)

32
domain/abri/models.py Normal file
View file

@ -0,0 +1,32 @@
from dataclasses import dataclass
from datetime import date
from typing import Literal, NewType, Union
PlaceRef = NewType("PlaceRef", str)
SlotCode = Literal["AM", "PM", "AD"]
@dataclass(frozen=True)
class LogJobRequest:
client_ref: str
place_ref: PlaceRef
appointment_date: date
appointment_time: SlotCode
short_description: str
long_description: str
@dataclass(frozen=True)
class JobLogged:
job_no: str
logged_info: str
@dataclass(frozen=True)
class AbriRequestRejected:
code: str
message: str
LogJobResult = Union[JobLogged, AbriRequestRejected]

13
domain/abri/slots.py Normal file
View file

@ -0,0 +1,13 @@
from datetime import datetime, time
from typing import Optional
from domain.abri.models import SlotCode
_MIDDAY = time(12, 0)
def slot_for_confirmed_time(confirmed_time: Optional[str]) -> SlotCode:
if confirmed_time is None or confirmed_time.strip() == "":
return "AD"
parsed = datetime.strptime(confirmed_time.strip(), "%H:%M").time()
return "AM" if parsed < _MIDDAY else "PM"

View file

@ -0,0 +1,42 @@
from __future__ import annotations
from enum import Enum
from typing import Callable, Optional, TypeVar
from domain.data_transformation.column_classifier import ColumnClassifier
E = TypeVar("E", bound=Enum)
class GuardedColumnClassifier(ColumnClassifier[E]):
"""A ``ColumnClassifier`` that resolves the descriptions a deterministic guard
is certain about, and delegates the rest to a fallback classifier.
The ``guard`` maps a raw description to a category member when it recognises it
deterministically (e.g. a party-ceiling roof marker #1376), else ``None``.
Guard hits never reach the fallback, so an unreliable classifier (the LLM)
cannot override a description the guard is sure of and the LLM is not billed
for it. Every description still appears in the result (guarded or fallen-back).
"""
def __init__(
self,
guard: Callable[[str], Optional[E]],
fallback: ColumnClassifier[E],
) -> None:
self._guard = guard
self._fallback = fallback
def classify(self, descriptions: set[str]) -> dict[str, E]:
guarded: dict[str, E] = {}
misses: set[str] = set()
for description in descriptions:
member = self._guard(description)
if member is not None:
guarded[description] = member
else:
misses.add(description)
# Only the misses reach the fallback — a fully-guarded batch never calls it.
if misses:
guarded.update(self._fallback.classify(misses))
return guarded

View file

@ -24,6 +24,7 @@ _GLAZING_CODES: dict[str, int] = {
"Double glazing, pre-2002": 3,
"Triple glazing, pre-2002": 6,
"Triple glazing, 2002 or later": 9,
"Secondary glazing": 5,
}

View file

@ -28,12 +28,16 @@ _FUEL_CODES: dict[str, int] = {
"house coal": 33,
"smokeless coal": 15,
"dual fuel (mineral and wood)": 10,
# Individual wood-logs solid fuel — API fuel enum 6 (RdSAP Table 32 code 20,
# 0.028 kgCO2e/kWh), matching the water overlay's `wood logs` fuel. Distinct
# from biomass (community) 31; the LLM funnelled it there for want of a member.
"wood logs": 6,
"biomass (community)": 31,
}
# A "mains gas" main fuel asserts the dwelling has a mains-gas connection, so the
# overlay must also flip `sap_energy_source.mains_gas` — not just the fuel code.
# Without it the effective EPC says "fuel = mains gas" while `mains_gas` stays
# overlay must also flip `sap_energy_source.gas_connection_available` — not just the fuel code.
# Without it the effective EPC says "fuel = mains gas" while `gas_connection_available` stays
# False, which (a) gates out the gas-boiler-upgrade Measure and (b) makes the
# heating Generator read the dwelling as off-gas and wrongly offer HHRSH storage
# (property 728513). Only the **private** mains-gas connection (code 26) sets it;
@ -51,7 +55,7 @@ def fuel_overlay_for(
code = _FUEL_CODES.get(main_fuel_value)
if code is None:
return None
mains_gas = True if main_fuel_value in _MAINS_GAS_FUEL_VALUES else None
gas_connection_available = True if main_fuel_value in _MAINS_GAS_FUEL_VALUES else None
return EpcSimulation(
heating=HeatingOverlay(main_fuel_type=code, mains_gas=mains_gas)
heating=HeatingOverlay(main_fuel_type=code, gas_connection_available=gas_connection_available)
)

View file

@ -12,10 +12,11 @@ field-wise with the main_fuel / water_heating overlays.
electricity tariff (meter) and, for storage heaters, its charge control. Rather
than hand-attach those per archetype (easy to forget when a new system is
added), they are **derived from the SAP code**: the off-peak meter from the
calculator's single off-peak classification (`OFF_PEAK_IMPLYING_HEATING_CODES`,
SAP §12), and the conservative manual charge control for storage heaters. So
adding a heating archetype is just adding its code coherent companions fall
out. Synthesis owns coherence; the calculator never normalises a lodged cert.
overlay's assumed-Dual classification (`_ASSUMED_DUAL_METER_CODES` — the §12
off-peak systems plus all-electric room-heater dwellings), and the conservative
manual charge control for storage heaters. So adding a heating archetype is just
adding its code coherent companions fall out. Synthesis owns coherence; the
calculator never normalises a lodged cert.
The SEDBUK A-G efficiency band the Hyde "Heating" column carries is NOT honoured
yet (no efficiency slot on the overlay/MainHeatingDetail) -- archetypes map to
@ -27,6 +28,7 @@ are left UNKNOWN until modelled. Unresolvable values produce no overlay.
from __future__ import annotations
import logging
from typing import Optional
from domain.modelling.simulation import EpcSimulation, HeatingOverlay
@ -34,6 +36,8 @@ from domain.sap10_calculator.tables.table_12a import (
OFF_PEAK_IMPLYING_HEATING_CODES,
)
logger = logging.getLogger(__name__)
# Off-peak (Economy 7) meter. Electric storage / CPSU systems charge overnight at
# the low rate and cannot run economically on a single-rate meter; "Dual" lets
# the §12 dispatch resolve the specific tariff (storage 7-hour, CPSU 10-hour).
@ -44,6 +48,23 @@ _OFF_PEAK_METER = "Dual"
# meter bleed through and bill the new gas/direct-acting system on an Economy-7
# split (the mirror of the storage→Dual drag, ADR-0035).
_SINGLE_RATE_METER = "Single"
# Meter-agnostic codes: the archetype cannot determine the tariff, so the overlay
# defers `meter_type` to the cert (`_meter_for` → None). Screed underfloor (424) is
# "standard or off peak" — 81% of the corpus lodge off-peak (ADR-0046).
_METER_AGNOSTIC_CODES = frozenset({424})
# Electric room heaters (SAP Table 4a 691). They don't *require* off-peak the way
# storage/CPSU do, so they're absent from the calculator's §12
# `OFF_PEAK_IMPLYING_HEATING_CODES` (Rules 1-2). But a dwelling heated by them is
# all-electric and realistically billed on Economy 7 — its immersion hot water
# charges overnight and §12 Rule 3 gives the room heaters a 10-hour off-peak
# window. So when the landlord names only the system, the coherent meter to
# assume is Dual; the §12 dispatch then applies the realistic high/low split
# (not a single-rate over-penalty, nor an all-low over-credit).
_ROOM_HEATER_CODES = frozenset({691})
# Codes for which the overlay assumes a Dual (off-peak) meter: the §12-mandated
# off-peak systems plus the all-electric room-heater dwellings above.
_ASSUMED_DUAL_METER_CODES = OFF_PEAK_IMPLYING_HEATING_CODES | _ROOM_HEATER_CODES
# SAP Table 4e Group 4 storage charge-control code. Manual charge control is the
# *conservative* assumption when the landlord didn't tell us the control: its
@ -53,6 +74,94 @@ _SINGLE_RATE_METER = "Single"
# systems that take a charge control.
_MANUAL_CHARGE_CONTROL = 2401
_STORAGE_HEATER_CODES = frozenset(range(401, 410))
# SAP Table 4a "electric storage heaters" category. Set on a storage-system
# override so the overlaid cert is internally consistent (code 401-409 ↔ category
# 7) — mirroring the control/fuel/meter companions the overlay already drags. A
# storage code with a stale non-storage category is a latent trap: the SAP 10.2
# Table 12a resolver classifies by category, so a leftover room-heater category
# 10 would price the off-peak storage heating at the peak rate.
_STORAGE_HEATER_CATEGORY = 7
# High heat retention storage heaters (SAP Table 4a 409) take a SINGLE intrinsic
# charge control — Table 4e 2404 (HHR, 0 C adjustment). It is named by the
# archetype, not unobserved, so 409 drags 2404 rather than the manual default
# (ADR-0044) — the only storage code that overrides `_MANUAL_CHARGE_CONTROL`.
_HHR_STORAGE_CODE = 409
_HHR_CHARGE_CONTROL = 2404
# SAP Table 4a category 10 ("Room heaters") and its conservative Table 4e Group 6
# control. A landlord names a room heater, not its control; code 2601 ("no
# thermostatic control of room temperature") is the lowest-SAP room-heater
# control — the modal one real solid-fuel room-heater certs lodge — so it never
# over-credits an unobserved control (the room-heater mirror of the storage
# manual-charge default). Scoped per family as archetypes land; solid-fuel room
# heaters are Table 4a 631-636.
_ROOM_HEATER_CATEGORY = 10
_ROOM_HEATER_CONTROL = 2601
_SOLID_FUEL_ROOM_HEATER_CODES = frozenset(range(631, 637))
# Oil room heaters (SAP Table 4a 621-625) — category 10, conservative room-heater
# control, natural fuel heating oil (RdSAP main_fuel 28).
_OIL_ROOM_HEATER_CODES = frozenset(range(621, 626))
_OIL_FUEL = 28
# Gas (incl. LPG) room heaters (SAP Table 4a 601-613) — category 10, conservative
# control, natural fuel mains gas (26); an LPG dwelling is refined by a main_fuel
# override (the overlay can't see the mains connection).
_GAS_ROOM_HEATER_CODES = frozenset(range(601, 614))
# Fuel-burning room heaters (solid + oil + gas) share category 10 + the
# conservative room-heater control; only the natural fuel differs by family.
# (Distinct from `_ROOM_HEATER_CODES` above, the electric-room-heater dual-meter
# set.)
_CATEGORY_10_ROOM_HEATER_CODES = (
_SOLID_FUEL_ROOM_HEATER_CODES | _OIL_ROOM_HEATER_CODES | _GAS_ROOM_HEATER_CODES
)
# Natural-fuel coherence (ADR-0041): where an archetype unambiguously implies a
# fuel, the overlay drags it so a system-only override is self-coherent on a cert
# that lodged a different fuel. A later `main_fuel` override still wins (last-wins
# composition); a contradicting landlord fuel is logged, not silently overridden.
# Electric room heaters (Table 4a 691-701) are unambiguously electricity (RdSAP
# main_fuel code 29). Solid fuel is ambiguous (coal / anthracite / smokeless /
# dual fuel / wood logs / pellets), so it defaults to house coal (33) — the most
# common solid fuel — when the landlord gives no `main_fuel` override; a specific
# solid-fuel override still wins by last-wins composition.
_ELECTRICITY_FUEL = 29
_HOUSE_COAL_FUEL = 33
_ELECTRIC_ROOM_HEATER_CODES = frozenset(range(691, 702))
# Electric boilers — SAP Table 4a 191 (direct-acting) and 192 (electric CPSU) —
# unambiguously electric, so they drag electricity like the electric room heaters
# (ADR-0045, closing the prior no-fuel gap on 191/192).
_ELECTRIC_BOILER_CODES = frozenset({191, 192})
# Electric underfloor — SAP Table 4a 421/422 (off-peak) and 424 (screed) — also
# unambiguously electric (ADR-0046).
_ELECTRIC_UNDERFLOOR_CODES = frozenset({421, 422, 424})
# SAP Table 4a "electric underfloor heating" category. Stamped so an underfloor
# override never leaves the replaced system's category behind — property 711795
# (PRD #1435 WS2) kept a replaced heat network's category 6, which the heating
# generator's `is_heat_network_main` gate read as community heating: no heating
# upgrade was offered at all and the baseline mis-rated 51 vs 67 (ADR-0050).
_UNDERFLOOR_CATEGORY = 8
# SAP Table 4e Group 7 underfloor control: 2701 ("no time or thermostatic
# control of room temperature", +0.3 C) is the conservative default when the
# landlord named only the system — the largest mean-internal-temperature
# adjustment in the group, so an unobserved control is never over-credited
# (the underfloor mirror of storage-manual 2401 and room-heater 2601, ADR-0050).
_UNDERFLOOR_CONTROL = 2701
# Heat pumps (SAP Table 4a 211-224 wet, 521-527 warm-air) are category 4 and
# unambiguously electric (natural fuel 29). Modellable on the default code's SPF
# without a PCDB index (ADR-0041).
_HEAT_PUMP_CATEGORY = 4
_HEAT_PUMP_CODES = frozenset(range(211, 225)) | frozenset(range(521, 528))
# Community / heat-network heating (SAP Table 4a 301-304) is category 6; the
# calculator's `_is_heat_network` keys off code OR category 6. The boiler-driven
# schemes (301/302/303) are dominantly mains gas (community) — RdSAP main_fuel 20
# — per real community certs; the heat-pump scheme (304) is electricity
# (community), added with its archetype (ADR-0041).
_HEAT_NETWORK_CATEGORY = 6
_HEAT_NETWORK_CODES = frozenset({301, 302, 303, 304})
_COMMUNITY_BOILER_CODES = frozenset({301, 302, 303})
_COMMUNITY_GAS_FUEL = 20
# SAP Table 4c full boiler-control code: programmer + room thermostat + TRVs. The
# landlord names the boiler, not its controls — but a gas boiler installed under
@ -80,9 +189,11 @@ _FROM_MAIN_WATER_HEATING_CODE = 901
# Canonical system archetype → representative SAP `sap_main_heating_code`. Codes
# map to the modern/condensing variant (A-G efficiency deferred): 102 regular
# condensing, 104 condensing combi, 120 CPSU, 401-404 storage heaters, 191
# direct-acting electric. Companion fields (meter / control / fuel / hot water)
# are NOT listed here — they are derived from the code below, so a new archetype
# is just a code (ADR-0035 drag-along).
# direct-acting electric, 691 panel/convector/radiant electric room heaters
# (Table 4a — direct-acting, so a single-rate meter, NOT off-peak storage).
# Companion fields (meter / control / fuel / hot water) are NOT listed here —
# they are derived from the code below, so a new archetype is just a code
# (ADR-0035 drag-along).
_MAIN_HEATING_CODES: dict[str, int] = {
"Gas boiler, combi": 104,
"Gas boiler, regular": 102,
@ -91,15 +202,73 @@ _MAIN_HEATING_CODES: dict[str, int] = {
"Electric storage heaters, slimline": 402,
"Electric storage heaters, convector": 403,
"Electric storage heaters, fan": 404,
# High heat retention storage heaters — SAP Table 4a 409. Its intrinsic HHR
# charge control (2404) and electricity fuel drag from the code below, not the
# generic manual-storage default (ADR-0044).
"Electric storage heaters, high heat retention": 409,
"Direct-acting electric": 191,
# A-rated electric boiler off the Gas CPSU dumping ground — SAP Table 4a 191
# (direct-acting electric boiler). Unambiguously electric, so it drags
# electricity below (ADR-0045).
"Electric boiler": 191,
# Electric CPSU — SAP Table 4a 192. Electric (drags 29), on an off-peak Dual
# meter (already in _ASSUMED_DUAL_METER_CODES; §12 Rule 1 10-hour) — ADR-0045.
"Electric CPSU": 192,
# Electric underfloor — dedicated SAP Table 4a codes, all electric (drag 29).
# 421 concrete slab / 422 integrated are off-peak (Dual, §12 Rule 2); 424
# screed is tariff-ambiguous ("standard or off peak") so it DEFERS the meter to
# the cert (ADR-0046).
"Electric underfloor, in concrete slab (off-peak)": 421,
"Electric underfloor, integrated storage and direct-acting": 422,
"Electric underfloor, in screed above insulation": 424,
"Electric room heaters": 691,
"Solid fuel room heater, closed": 633,
# Solid-fuel room heaters off the Gas CPSU dumping ground (ADR-0045). SAP
# Table 4a: 631 open fire, 632 open fire + back boiler, 634 closed + boiler.
# Companions (house-coal fuel 33, room-heater control 2601, category 10) drag
# from the code — 631/632/634 are already in the 631-636 solid-fuel set.
"Solid fuel room heater, open fire": 631,
"Solid fuel room heater, open fire with back boiler": 632,
"Solid fuel room heater, closed with boiler": 634,
# Default air-source heat pump — SAP Table 4a 211 ("flow temp in other
# cases", default SPF 2.30). Modellable without a PCDB index; an actual
# product index would refine it (ADR-0041).
"Air source heat pump": 211,
# Boiler-driven community / heat-network heating — SAP Table 4a 301. The
# calculator derives the heat-source efficiency (80%) + DLF (1.50 default)
# from the code (cert_to_inputs). A generic "Community heating" with no named
# source stays unmapped (-> None) — the source can't be assumed (ADR-0041).
"Community heating, boilers": 301,
# Community CHP + boilers — SAP Table 4a 302 (heat network, category 6).
"Community heating, CHP and boilers": 302,
# Modern standalone oil room heater — SAP Table 4a 623 ("Oil room heater,
# 2000 or later", no boiler). Natural fuel heating oil (ADR-0041).
"Oil room heater, 2000 or later": 623,
# Gas room heaters — each catalogue variant to its own SAP Table 4a code
# (wide efficiency spread, so no single representative). Natural fuel mains
# gas; an LPG dwelling is refined by a `main_fuel` override (ADR-0041).
"Gas room heater, condensing fire": 611,
"Gas room heater, decorative fuel-effect": 612,
"Gas room heater, flush live-effect": 605,
"Gas room heater, open flue 1980 or later": 603,
"Gas room heater, open flue pre-1980": 601,
}
def _meter_for(code: int) -> str:
def _meter_for(code: int) -> Optional[str]:
"""The coherent meter a heating code implies: an off-peak ("Dual") meter for
the calculator's §12 off-peak systems, an explicit single-rate ("Single")
meter for every other system. Always set never left to bleed."""
return _OFF_PEAK_METER if code in OFF_PEAK_IMPLYING_HEATING_CODES else _SINGLE_RATE_METER
the §12 off-peak systems and all-electric room-heater dwellings
(`_ASSUMED_DUAL_METER_CODES`), an explicit single-rate ("Single") meter for
every other system set explicitly so it never bleeds.
Exception: a **meter-agnostic** code (`_METER_AGNOSTIC_CODES` screed
underfloor 424, "standard or off peak") returns ``None`` so the overlay leaves
`meter_type` unset and the cert's lodged tariff stands. The archetype genuinely
cannot know the tariff, and unlike a switched-off storage system it is not a
re-metering, so deferring is faithful, not a bleed (ADR-0046)."""
if code in _METER_AGNOSTIC_CODES:
return None
return _OFF_PEAK_METER if code in _ASSUMED_DUAL_METER_CODES else _SINGLE_RATE_METER
def _control_for(code: int) -> Optional[int]:
@ -107,10 +276,61 @@ def _control_for(code: int) -> Optional[int]:
conservative manual charge control for storage heaters, full modern controls
for a gas boiler, None for systems that take neither (direct-acting electric).
Overwrites a stale control inherited from the system being replaced."""
if code == _HHR_STORAGE_CODE:
return _HHR_CHARGE_CONTROL
if code in _STORAGE_HEATER_CODES:
return _MANUAL_CHARGE_CONTROL
if code in _GAS_BOILER_CODES:
return _FULL_BOILER_CONTROL
if code in _CATEGORY_10_ROOM_HEATER_CODES or code in _ELECTRIC_ROOM_HEATER_CODES:
return _ROOM_HEATER_CONTROL
if code in _ELECTRIC_UNDERFLOOR_CODES:
return _UNDERFLOOR_CONTROL
return None
def _category_for(code: int) -> Optional[int]:
"""The SAP Table 4a heating category a code implies, where the archetype
fixes it. Set so a system-only override reads as the new system's category,
not whatever category the replaced system carried room heaters are 10,
electric storage heaters (incl. HHR 409) are 7, heat pumps 4, heat networks
6. Setting storage 7 keeps the overlaid cert consistent (code 401-409
category 7); without it a storage override left a stale category behind,
which the SAP 10.2 Table 12a resolver (keyed on category) could misread as a
non-storage system and price the off-peak storage heating at the peak rate."""
if code in _CATEGORY_10_ROOM_HEATER_CODES or code in _ELECTRIC_ROOM_HEATER_CODES:
return _ROOM_HEATER_CATEGORY
if code in _STORAGE_HEATER_CODES:
return _STORAGE_HEATER_CATEGORY
if code in _ELECTRIC_UNDERFLOOR_CODES:
return _UNDERFLOOR_CATEGORY
if code in _HEAT_PUMP_CODES:
return _HEAT_PUMP_CATEGORY
if code in _HEAT_NETWORK_CODES:
return _HEAT_NETWORK_CATEGORY
return None
def _natural_fuel_for(code: int) -> Optional[int]:
"""The fuel an archetype unambiguously implies (a coherent default), or None
where the fuel is ambiguous and must come from the `main_fuel` override. A
present `main_fuel` override wins by last-wins composition."""
if (
code in _ELECTRIC_ROOM_HEATER_CODES
or code in _HEAT_PUMP_CODES
or code == _HHR_STORAGE_CODE
or code in _ELECTRIC_BOILER_CODES
or code in _ELECTRIC_UNDERFLOOR_CODES
):
return _ELECTRICITY_FUEL
if code in _SOLID_FUEL_ROOM_HEATER_CODES:
return _HOUSE_COAL_FUEL
if code in _OIL_ROOM_HEATER_CODES:
return _OIL_FUEL
if code in _GAS_ROOM_HEATER_CODES:
return _MAINS_GAS_FUEL
if code in _COMMUNITY_BOILER_CODES:
return _COMMUNITY_GAS_FUEL
return None
@ -123,7 +343,7 @@ def _gas_boiler_overlay(code: int) -> HeatingOverlay:
sap_main_heating_code=code,
main_heating_category=_GAS_BOILER_CATEGORY,
main_fuel_type=_MAINS_GAS_FUEL,
mains_gas=True,
gas_connection_available=True,
main_heating_control=_FULL_BOILER_CONTROL,
fan_flue_present=True,
meter_type=_SINGLE_RATE_METER,
@ -133,6 +353,16 @@ def _gas_boiler_overlay(code: int) -> HeatingOverlay:
)
def natural_fuel_for(main_heating_value: str) -> Optional[int]:
"""The RdSAP `main_fuel` code a heating archetype unambiguously implies — its
natural fuel (ADR-0041) or None for an unmapped archetype. Exposed so a
plausibility check can compare it against a landlord `main_fuel` override."""
code = _MAIN_HEATING_CODES.get(main_heating_value)
if code is None:
return None
return _natural_fuel_for(code)
def main_heating_overlay_for(
main_heating_value: str, building_part: int
) -> Optional[EpcSimulation]:
@ -141,10 +371,36 @@ def main_heating_overlay_for(
return None
if code in _GAS_BOILER_CODES:
return EpcSimulation(heating=_gas_boiler_overlay(code))
category = _category_for(code)
control = _control_for(code)
if category is None or control is None:
# A system-replacing override should stamp a complete, coherent companion
# set; an unmapped category or control leaves the field unset, so the
# effective cert inherits the REPLACED system's value (a silent mis-rating
# — e.g. a room heater keeping a storage heater's category/control). We do
# not yet have confident defaults for every archetype, so log-and-continue
# (matching `flag_fuel_mismatch`) to make the gap visible for review
# rather than raise mid-run.
logger.error(
"Landlord main_heating_system %r (SAP code %d) overlaid with an "
"incomplete companion set (category=%s, control=%s); the effective "
"cert may inherit the replaced system's value — known gap, review",
main_heating_value,
code,
category,
control,
)
return EpcSimulation(
heating=HeatingOverlay(
sap_main_heating_code=code,
main_heating_category=category,
main_fuel_type=_natural_fuel_for(code),
meter_type=_meter_for(code),
main_heating_control=_control_for(code),
main_heating_control=control,
# A landlord override describes the existing dwelling, so its assumed
# off-peak meter must not downgrade a more-off-peak cert meter
# (e.g. a 24-hour all-low tariff). Measures, which re-meter for real,
# build their HeatingOverlay directly and leave this False.
keep_existing_off_peak_meter=True,
)
)

View file

@ -27,6 +27,8 @@ from datatypes.epc.domain.epc_property_data import BuildingPartIdentifier
from domain.modelling.simulation import BuildingPartOverlay, EpcSimulation
_LOFT_MM = re.compile(r"(\d+)\+?\s*mm loft insulation")
# A flat roof carries no loft, so its depth reads "N mm insulation" (no "loft").
_FLAT_MM = re.compile(r"(\d+)\+?\s*mm insulation")
def roof_overlay_for(
@ -49,8 +51,18 @@ def _overlay_for(roof_type_value: str) -> Optional[BuildingPartOverlay]:
if match is not None:
return BuildingPartOverlay(roof_insulation_thickness=int(match.group(1)))
if roof_type_value.startswith("Flat,"):
# Flat roof: U-value is the age-band default; flag the shape and let the
# age-band overlay drive `_FLAT_ROOF_BY_AGE` (ADR-0033).
flat_depth = _FLAT_MM.search(roof_type_value)
if flat_depth is not None:
# A flat roof with a known depth: RdSAP 10 Table 16 col (1) ("joists at
# ceiling level and flat roof") scores it by thickness, so carry the
# depth (the calculator routes flat + thickness through Table 16, not
# the age-band default). ADR-0041.
return BuildingPartOverlay(
roof_construction_type="Flat",
roof_insulation_thickness=int(flat_depth.group(1)),
)
# Flat roof with no stated depth: U-value is the age-band default; flag the
# shape and let the age-band overlay drive `_FLAT_ROOF_BY_AGE` (ADR-0033).
return BuildingPartOverlay(roof_construction_type="Flat")
if roof_type_value == "Pitched, Unknown loft insulation":
# Unknown loft depth: U-value is the pitched age-band default. Assert the

View file

@ -31,6 +31,9 @@ _MATERIAL_CONSTRUCTION: dict[str, int] = {
"Curtain Wall": 9,
}
# RdSAP `WALL_SYSTEM_BUILT` — shares code 6 with the gov-EPC basement sentinel.
_WALL_SYSTEM_BUILT = 6
# RdSAP `wall_insulation_type` codes by insulation-state suffix
# (domain/sap10_ml/rdsap_uvalues.py): external 1, filled-cavity 2, internal 3,
# as-built/uninsulated 4, cavity+external 6, cavity+internal 7.
@ -66,11 +69,18 @@ def wall_overlay_for(
if building_part == 0
else BuildingPartIdentifier.extension(building_part)
)
# System-built is RdSAP code 6, which collides with the gov-EPC code-6
# basement sentinel that `main_wall_is_basement` falls back to. A landlord
# naming a System-built wall is asserting the material, not a basement — pin
# the flag False so the override isn't mis-read as one (ADR-0033 / the
# overlay mirror of the mapper's `_clear_basement_flag_when_system_built`).
wall_is_basement = False if construction == _WALL_SYSTEM_BUILT else None
return EpcSimulation(
building_parts={
identifier: BuildingPartOverlay(
wall_construction=construction,
wall_insulation_type=insulation,
wall_is_basement=wall_is_basement,
)
}
)

View file

@ -26,6 +26,18 @@ _WATER_HEATING_CODES: dict[str, tuple[int, int]] = {
"From main system, LPG (bulk)": (901, 27),
"From main system, bottled LPG": (901, 3),
"From main system, house coal": (901, 33),
# Biomass / wood water fuels routed off the house-coal dumping ground
# (ADR-0043), all from main (901) with their own RdSAP water_heating_fuel:
# 6 wood logs (biomass ~0.028 kgCO2/kWh vs house coal 33 ~0.395)
# 9 dual fuel (mineral + wood)
# 31 biomass (community) — scored via the coherent community main; the
# fuel is still emitted so an individual main would score biomass
# 34 biodiesel (community)
# A wood-fired individual-main dwelling's DHW must never fall to coal 33.
"From main system, wood logs": (901, 6),
"From main system, dual fuel (mineral and wood)": (901, 9),
"From main system, biomass (community)": (901, 31),
"From main system, biodiesel (community)": (901, 34),
"Electric immersion, electricity": (903, 29),
# "boiler/circulator for water heating only" — SAP Table 4a code 911 (gas).
"Gas boiler/circulator, mains gas": (911, 26),

View file

@ -0,0 +1,82 @@
from __future__ import annotations
import re
from typing import Optional
from domain.epc.property_overrides.glazing_type import GlazingType
# "N% <base glazing type> [era]" — the base type decides whether it is a mix; the
# era ("2002 or later" / "pre-2002") is captured too, so a dominant double/triple
# can be resolved deterministically when its era is stated (see the guard below).
# An absent or non-canonical era ("unknown age", "(SAP 9.94)") leaves the era group
# empty, so that dominant split stays the LLM's job.
_PERCENT_TYPE = re.compile(
r"(\d+)%\s*(single|double|triple|secondary)(?:\s*glazing)?"
r"(?:[\s,]*(pre-?2002|2002 or later))?"
)
# One base type at or above this share is a uniform assertion, not a mix — applied
# as that type (a near-uniform reglaze), not deferred.
_UNIFORM_THRESHOLD_PERCENT = 90
# A dominant base type + stated era → the fully-determined canonical member.
# Single and secondary glazing are era-free, so they need no era. Double/triple
# with no (or non-canonical) era is absent here and falls through to the LLM.
_DOMINANT_MEMBER: dict[tuple[str, Optional[str]], GlazingType] = {
("single", None): GlazingType.SINGLE,
("secondary", None): GlazingType.SECONDARY,
("double", "2002 or later"): GlazingType.DOUBLE_POST_2002,
("double", "pre-2002"): GlazingType.DOUBLE_PRE_2002,
("triple", "2002 or later"): GlazingType.TRIPLE_POST_2002,
("triple", "pre-2002"): GlazingType.TRIPLE_PRE_2002,
}
def _canonical_era(era: str) -> Optional[str]:
"""Normalise a matched era phrase to the key used in ``_DOMINANT_MEMBER``."""
if not era:
return None
return "2002 or later" if "2002 or later" in era else "pre-2002"
def glazing_mix_guard(description: str) -> Optional[GlazingType]:
"""Deterministically resolve an aggregate-mix glazing description to ``MIXED``.
A landlord glazing description is often a whole-dwelling proportion split
("40% double glazing 2002 or later, 60% single glazing"). Such a mix cannot be
faithfully applied per-window it says *how much* is each type, not *which*
windows and the cert already carries the true per-window glazing, so a mix
must resolve to ``GlazingType.MIXED`` (no overlay the cert is kept), never to
a single dominant type that would flatten every window (ADR-0042).
Recognises the structured ``"N% <type>, M% <type>"`` split and returns:
* ``MIXED`` for a genuine mix two or more glazing types present and none
dominant ( the uniform threshold);
* the fully-determined member for a dominant near-uniform split whose type
carries no era ambiguity single or secondary glazing (era-free), or a
dominant double/triple whose era is stated ("pre-2002" / "2002 or later");
the LLM would otherwise flatten such a split onto its *minority* type;
* ``None`` otherwise an unparseable description, or a dominant double/triple
with an unstated/non-canonical era ("unknown age") that still carries genuine
era ambiguity so the LLM classifier resolves it.
"""
matches = _PERCENT_TYPE.findall(description.lower())
base_types = {base_type for _, base_type, _ in matches}
if len(base_types) < 2:
# One (or no) glazing type named — a uniform assertion, not a mix.
return None
dominant_percent, dominant_type, dominant_era = max(
((int(percent), base_type, era) for percent, base_type, era in matches),
key=lambda triple: triple[0],
)
if dominant_percent >= _UNIFORM_THRESHOLD_PERCENT:
# One type dominates — a near-uniform assertion, not a mix. A split is fully
# determined (and safe to apply deterministically) when the dominant type
# carries no era ambiguity: single glazing is era-free, and a dominant
# double/triple whose era is stated ("pre-2002" / "2002 or later") is
# equally pinned. In those cases claim the member ourselves — the LLM
# otherwise flattens the split onto the *minority* type. A dominant
# double/triple with an unstated or non-canonical era ("unknown age") still
# carries genuine era ambiguity, so it stays the LLM's job (falls through to
# ``None``).
return _DOMINANT_MEMBER.get((dominant_type, _canonical_era(dominant_era)))
return GlazingType.MIXED

View file

@ -21,4 +21,15 @@ class GlazingType(Enum):
DOUBLE_PRE_2002 = "Double glazing, pre-2002"
TRIPLE_PRE_2002 = "Triple glazing, pre-2002"
TRIPLE_POST_2002 = "Triple glazing, 2002 or later"
# An aggregate mix of glazing types across the dwelling ("40% double, 60%
# single"). Deliberately absent from the overlay's `_GLAZING_CODES`, so it
# resolves to no overlay and the cert's per-window `sap_windows` are kept
# rather than flattened to one type — a whole-dwelling proportion cannot say
# which windows are which (ADR-0042 amendment, #1376). The FE-owned pgEnum
# gains this member via a Drizzle migration.
MIXED = "Mixed glazing"
# Secondary glazing (a pane added inside the original window) is era-free like
# SINGLE — SAP10 glazing code 5, U = 2.9 regardless of install year. The
# FE-owned pgEnum gains this member via assessment-model#345.
SECONDARY = "Secondary glazing"
UNKNOWN = "Unknown"

View file

@ -0,0 +1,28 @@
from __future__ import annotations
from typing import Optional
from domain.epc.property_overrides.main_fuel_type import MainFuelType
def main_fuel_guard(description: str) -> Optional[MainFuelType]:
"""Deterministically resolve an individual wood-logs main fuel, which the LLM
funnelled into ``"biomass (community)"`` for want of a wood-logs member (#1376).
A landlord main-fuel description arrives as ``"<family>: <fuel>"`` (e.g.
``"Solid Fuel: Wood Logs"``). With no individual wood-logs archetype the
classifier force-picked ``BIOMASS_COMMUNITY`` inventing a community heat
network the dwelling isn't on (0.028 kgCO2e/kWh is right by luck, but the
community flag mis-costs it and mislabels the connection). Wood logs is a real
RdSAP solid fuel (API enum 6 / Table 32 code 20), so this guard claims it.
Mirrors ``water_heating_guard`` (which already rescues the same fuel token for
DHW, ADR-0043). Returns ``None`` for fuels the LLM already classifies correctly
(mains gas, electricity, oil, dual fuel, genuine community biomass) and for
varied phrasings, so those still reach the LLM via ``GuardedColumnClassifier``.
``"dual fuel: mineral and wood"`` contains ``"wood"`` but not ``"wood log"``,
so it keeps its own ``DUAL_FUEL_MINERAL_WOOD`` member.
"""
if "wood log" in description.lower():
return MainFuelType.WOOD_LOGS
return None

View file

@ -25,5 +25,6 @@ class MainFuelType(Enum):
HOUSE_COAL = "house coal"
SMOKELESS_COAL = "smokeless coal"
DUAL_FUEL_MINERAL_WOOD = "dual fuel (mineral and wood)"
WOOD_LOGS = "wood logs"
BIOMASS_COMMUNITY = "biomass (community)"
UNKNOWN = "Unknown"

View file

@ -0,0 +1,53 @@
from __future__ import annotations
from typing import Optional
from domain.epc.property_overrides.main_heating_system_type import (
MainHeatingSystemType,
)
def main_heating_guard(description: str) -> Optional[MainHeatingSystemType]:
"""Deterministically resolve the structured main-heating descriptions the LLM
funnelled into a garbage-drawer archetype for want of a correct target (#1376).
Two dumping grounds are rescued:
- **HHRSH** (ADR-0044): ``"…High heat retention storage heaters"`` went to
``"Electric storage heaters, old"`` (SAP 401) the *worst* old type. HHRSH is
SAP Table 4a 409 with its own intrinsic charge control (2404).
- **Gas CPSU** (ADR-0045): the mains-gas CPSU garbage drawer also held
solid-fuel room heaters (open fire 631 / + back boiler 632 / closed + boiler
634) and electric ``"NA"``-type boilers (SAP 191). "NA" is the boiler-type
value meaning "not a gas combi/regular/CPSU", i.e. an electric boiler.
- **Electric underfloor** (ADR-0046): dumped into Direct-acting (191) / old
storage (401) for want of the dedicated SAP underfloor codes in concrete
slab (421, off-peak), integrated storage+direct-acting (422), in screed (424,
meter deferred to the cert).
Returns ``None`` for descriptions the LLM already classifies correctly (genuine
gas boilers, other storage subtypes, a plain closed room heater) and for varied
phrasings, so they still reach the LLM via ``GuardedColumnClassifier``. Order
matters: the back-boiler open fire is tested before the bare open fire it
contains.
"""
text = description.lower()
if "high heat retention" in text:
return MainHeatingSystemType.ELECTRIC_STORAGE_HIGH_HEAT_RETENTION
if "underfloor" in text:
if "concrete slab" in text:
return MainHeatingSystemType.ELECTRIC_UNDERFLOOR_SLAB_OFF_PEAK
if "integrated" in text:
return MainHeatingSystemType.ELECTRIC_UNDERFLOOR_INTEGRATED
if "screed" in text:
return MainHeatingSystemType.ELECTRIC_UNDERFLOOR_SCREED
return None
if "rated na" in text:
return MainHeatingSystemType.ELECTRIC_BOILER
if "open fire" in text and "back boiler" in text:
return MainHeatingSystemType.SOLID_FUEL_ROOM_HEATER_OPEN_FIRE_BACK_BOILER
if "open fire" in text:
return MainHeatingSystemType.SOLID_FUEL_ROOM_HEATER_OPEN_FIRE
if "closed room heater" in text and "with boiler" in text:
return MainHeatingSystemType.SOLID_FUEL_ROOM_HEATER_CLOSED_WITH_BOILER
return None

View file

@ -23,5 +23,35 @@ class MainHeatingSystemType(Enum):
ELECTRIC_STORAGE_SLIMLINE = "Electric storage heaters, slimline"
ELECTRIC_STORAGE_CONVECTOR = "Electric storage heaters, convector"
ELECTRIC_STORAGE_FAN = "Electric storage heaters, fan"
ELECTRIC_STORAGE_HIGH_HEAT_RETENTION = (
"Electric storage heaters, high heat retention"
)
DIRECT_ELECTRIC = "Direct-acting electric"
ELECTRIC_BOILER = "Electric boiler"
ELECTRIC_CPSU = "Electric CPSU"
ELECTRIC_UNDERFLOOR_SLAB_OFF_PEAK = (
"Electric underfloor, in concrete slab (off-peak)"
)
ELECTRIC_UNDERFLOOR_INTEGRATED = (
"Electric underfloor, integrated storage and direct-acting"
)
ELECTRIC_UNDERFLOOR_SCREED = "Electric underfloor, in screed above insulation"
ELECTRIC_ROOM_HEATERS = "Electric room heaters"
SOLID_FUEL_ROOM_HEATER_CLOSED = "Solid fuel room heater, closed"
SOLID_FUEL_ROOM_HEATER_OPEN_FIRE = "Solid fuel room heater, open fire"
SOLID_FUEL_ROOM_HEATER_OPEN_FIRE_BACK_BOILER = (
"Solid fuel room heater, open fire with back boiler"
)
SOLID_FUEL_ROOM_HEATER_CLOSED_WITH_BOILER = (
"Solid fuel room heater, closed with boiler"
)
AIR_SOURCE_HEAT_PUMP = "Air source heat pump"
COMMUNITY_BOILERS = "Community heating, boilers"
COMMUNITY_CHP_AND_BOILERS = "Community heating, CHP and boilers"
OIL_ROOM_HEATER_POST_2000 = "Oil room heater, 2000 or later"
GAS_FIRE_CONDENSING = "Gas room heater, condensing fire"
GAS_FIRE_DECORATIVE = "Gas room heater, decorative fuel-effect"
GAS_FIRE_FLUSH_LIVE_EFFECT = "Gas room heater, flush live-effect"
GAS_FIRE_OPEN_FLUE_POST_1980 = "Gas room heater, open flue 1980 or later"
GAS_FIRE_OPEN_FLUE_PRE_1980 = "Gas room heater, open flue pre-1980"
UNKNOWN = "Unknown"

View file

@ -0,0 +1,32 @@
from __future__ import annotations
from typing import Optional
from domain.epc.property_overrides.property_type import PropertyType
# The landlord property-type description is a "<dwelling type>: <built form>: …"
# split (e.g. "Bungalow: EndTerrace"). The dwelling type is the leading token and
# is independent of the built-form tail — which the LLM sometimes over-reads,
# mislabelling a "Bungalow: EndTerrace" as House (#1376).
_LEADING_TYPE: dict[str, PropertyType] = {
"house": PropertyType.HOUSE,
"bungalow": PropertyType.BUNGALOW,
"flat": PropertyType.FLAT,
"maisonette": PropertyType.MAISONETTE,
"park home": PropertyType.PARK_HOME,
}
def property_type_guard(description: str) -> Optional[PropertyType]:
"""Deterministically resolve the dwelling type from the leading token of a
landlord property-type description.
The description is a structured ``"<dwelling type>: <built form>: <floor>"``
split whose first token is the dwelling type; the tail (EndTerrace, Mid Floor,
) is built form, not type. This guard claims the recognised leading tokens so
the built-form tail can never flip the type (the LLM read "Bungalow: EndTerrace"
as House). Returns ``None`` for an unrecognised leading token, so varied
phrasings still reach the LLM classifier via ``GuardedColumnClassifier``.
"""
leading = description.split(":", 1)[0].strip().lower()
return _LEADING_TYPE.get(leading)

View file

@ -0,0 +1,43 @@
from __future__ import annotations
import re
from typing import Optional
from domain.epc.property_overrides.roof_type import RoofType
# The roof-type token (before any ``: <insulation>`` suffix), normalised to lower
# alphanumerics, → its party-ceiling RoofType member. Normalising strips spacing
# and case so both ``"another dwelling above"`` and ``"anotherdwellingabove"``
# match the same marker.
_PARTY_CEILING_MARKERS: dict[str, RoofType] = {
"anotherdwellingabove": RoofType.ADJACENT_ANOTHER_DWELLING_ABOVE,
"samedwellingabove": RoofType.ADJACENT_SAME_DWELLING_ABOVE,
"otherpremisesabove": RoofType.ADJACENT_OTHER_PREMISES_ABOVE,
# Both the "(another premises above)" adjacency and the redundant "Another
# Premises Above" taxonomy member normalise here; map to the parenthesised
# family (both resolve to no overlay, so there is no scoring difference).
"anotherpremisesabove": RoofType.ADJACENT_ANOTHER_PREMISES_ABOVE,
}
def _normalise_roof_token(description: str) -> str:
token = description.split(":", 1)[0]
return re.sub(r"[^a-z0-9]", "", token.lower())
def roof_party_ceiling_guard(description: str) -> Optional[RoofType]:
"""Deterministically resolve a party-ceiling roof description to its RoofType.
A building part whose top boundary is another (or the same) dwelling / premises
above has ~0 heat loss (RdSAP 10 Table 18: "There is no heat loss through the
roof of a building part that has the same dwelling or another dwelling above"),
so it must resolve to a party-ceiling `RoofType` member which the roof
Simulation Overlay leaves as no overlay, keeping the lodged EPC and never to
an external `Pitched` / `Flat` roof.
Recognises the party-ceiling markers regardless of a trailing insulation token
(``: 100mm`` / ``: unknown``) or spacing/case, and returns ``None`` for anything
that is not a party-ceiling marker so the LLM classifier still handles it
(#1376).
"""
return _PARTY_CEILING_MARKERS.get(_normalise_roof_token(description))

View file

@ -22,6 +22,28 @@ class RoofType(Enum):
FLAT_NO_INSULATION = "Flat, no insulation"
FLAT_NO_INSULATION_ASSUMED = "Flat, no insulation (assumed)"
# A flat roof with a known insulation depth. RdSAP 10 Table 16 col (1)
# ("joists at ceiling level and flat roof") scores flat roofs by thickness, so
# these carry the depth into the overlay (mirroring the PITCHED_LOFT_* ladder;
# no "loft" — a flat roof has none). ADR-0041. The value column is an FE-owned
# pgEnum, so these members are added to the Drizzle enum by the FE owner.
FLAT_12MM = "Flat, 12 mm insulation"
FLAT_25MM = "Flat, 25 mm insulation"
FLAT_50MM = "Flat, 50 mm insulation"
FLAT_75MM = "Flat, 75 mm insulation"
FLAT_100MM = "Flat, 100 mm insulation"
FLAT_125MM = "Flat, 125 mm insulation"
FLAT_150MM = "Flat, 150 mm insulation"
FLAT_175MM = "Flat, 175 mm insulation"
FLAT_200MM = "Flat, 200 mm insulation"
FLAT_225MM = "Flat, 225 mm insulation"
FLAT_250MM = "Flat, 250 mm insulation"
FLAT_270MM = "Flat, 270 mm insulation"
FLAT_300MM = "Flat, 300 mm insulation"
FLAT_350MM = "Flat, 350 mm insulation"
FLAT_400MM = "Flat, 400 mm insulation"
FLAT_400_PLUS = "Flat, 400+ mm insulation"
PITCHED_INSULATED = "Pitched, insulated"
PITCHED_INSULATED_ASSUMED = "Pitched, insulated (assumed)"
PITCHED_INSULATED_AT_RAFTERS = "Pitched, insulated at rafters"

View file

@ -0,0 +1,41 @@
from __future__ import annotations
from typing import Optional
from domain.epc.property_overrides.water_heating_type import WaterHeatingType
def water_heating_guard(description: str) -> Optional[WaterHeatingType]:
"""Deterministically resolve the structured water-heating descriptions the LLM
funnelled into the ``"From main system, house coal"`` dumping ground (#1376).
Landlord water-heating descriptions arrive as ``"<system>: <fuel>"`` (e.g.
``"From main heating system: Wood Logs"``). Where the taxonomy had no biomass /
wood / dual-fuel / biodiesel water fuel, the classifier force-picked house coal,
scoring a biomass dwelling's DHW at ~14x its carbon (ADR-0043). This guard keys
on the **fuel token** and returns the faithful archetype the ``"Hot-water only
community scheme"`` system flattens to ``"From main system, "`` exactly as the
community mains-gas description already does. ``"No Heating or Hot Water"`` under
an ``"electric immersion assumed"`` system is the assessment's own immersion
assumption, so it resolves to ``ELECTRIC_IMMERSION``.
Returns ``None`` for fuels the LLM already classifies correctly (mains gas,
electricity, oil, ) and for varied / unstructured phrasings, so those still
reach the LLM classifier via ``GuardedColumnClassifier``. Order matters:
biodiesel and dual fuel are tested before the bare biomass / wood tokens they
contain.
"""
text = description.lower()
if "biodiesel" in text:
return WaterHeatingType.FROM_MAIN_BIODIESEL_COMMUNITY
if "biomass" in text:
return WaterHeatingType.FROM_MAIN_BIOMASS_COMMUNITY
if "dual fuel" in text:
return WaterHeatingType.FROM_MAIN_DUAL_FUEL_MINERAL_WOOD
if "wood log" in text:
return WaterHeatingType.FROM_MAIN_WOOD_LOGS
if "house coal" in text:
return WaterHeatingType.FROM_MAIN_HOUSE_COAL
if "no heating or hot water" in text:
return WaterHeatingType.ELECTRIC_IMMERSION
return None

View file

@ -21,6 +21,10 @@ class WaterHeatingType(Enum):
FROM_MAIN_LPG_BULK = "From main system, LPG (bulk)"
FROM_MAIN_BOTTLED_LPG = "From main system, bottled LPG"
FROM_MAIN_HOUSE_COAL = "From main system, house coal"
FROM_MAIN_WOOD_LOGS = "From main system, wood logs"
FROM_MAIN_DUAL_FUEL_MINERAL_WOOD = "From main system, dual fuel (mineral and wood)"
FROM_MAIN_BIOMASS_COMMUNITY = "From main system, biomass (community)"
FROM_MAIN_BIODIESEL_COMMUNITY = "From main system, biodiesel (community)"
ELECTRIC_IMMERSION = "Electric immersion, electricity"
GAS_BOILER_CIRCULATOR_MAINS_GAS = "Gas boiler/circulator, mains gas"
UNKNOWN = "Unknown"

View file

@ -99,8 +99,17 @@ def _map_window_ventilation(
opening_type=_try_get_str_value("Opening Type"),
num_openings=_try_get_int_value("Number of Openings (In Same Window)"),
pct_openable=_try_get_int_value("% of Window Openable"),
trickle_vent_area_mm2=_try_get_int_value(
"Trickle Vent Effective Area (mm2) (No Code Then Width x Height)"
trickle_vent_area_mm2=next(
(
v
for label in [
"Total Trickle Vent Effective Area (mm2) (No Code Then Width x Height)",
"Individual Trickle Vent Effective Area (mm2) (No Code Then Width x Height)",
"Trickle Vent Effective Area (mm2) (No Code Then Width x Height)",
]
if (v := _try_get_int_value(label)) is not None
),
None,
),
num_trickle_vents=_try_get_int_value("No. of Trickle Vents"),
)

Some files were not shown because too many files have changed in this diff Show more