Skip to content

fix(ppd): an exact house number is not ambiguous, and a partial one is not a match - #70

Merged
paulieb89 merged 1 commit into
mainfrom
fix/subject-property-exact-paon-wins
Sep 4, 2026
Merged

fix(ppd): an exact house number is not ambiguous, and a partial one is not a match#70
paulieb89 merged 1 commit into
mainfrom
fix/subject-property-exact-paon-wins

Conversation

@paulieb89

Copy link
Copy Markdown
Owner

Both sources match paon by substring, so "5 Alexandra Road" also returns
15, 25 and 5A. Two faults followed, and the uniqueness check solved neither.

1. "5" was refused — 14% of addresses

15 came back with it, the check saw two buildings, returned None. But 5 was
exactly one of the candidates and exactly what was asked for.

Measured across six outcodes of the real artifact: 14% of addresses, 1 in 7,
unresolvable this way — 5/15, 1/11/21, 7/17. In every case of that class the
exact match is present, because its presence is why the collision happened.

2. "2" was answered with 25 — a silent wrong answer

25 was the only partial match, and a single candidate passes a uniqueness
check unchallenged
. That is another property's sale history returned as yours,
its price feeding subject_price_percentile and subject_vs_median_pct.

Found by a test written for fault 1. It had been reachable the whole time, on
both paths.

The fix

Narrow to an exact paon before the uniqueness check. Not a relaxation:
nothing is selected that the query did not already return, and the check still
runs afterwards on what survives. Where nothing matches exactly, every candidate
merely contains the number asked for, so none is the property meant — None
is right.

_normalise_paon compares case and spacing only. Deliberately no more: "5" and
"5A" are different properties, and equating them would invent identity rather
than compare it.

Verified against the real artifact

5 Alexandra Road   -> 3 sales, latest 2026-01-23 £225,000   (was None)
9 Alexandra Road   -> 2 sales, latest 2022-12-05 £330,000   (was None)
27 Havenwood Rise  -> 1 sale                                (was None)
"Alexandra Road"   -> None        9999 Nowhere Street -> None

The 2026 sale matches the live answer retained on 2026-08-31. The 2008 and
2004 sales are history the eleven-year artifact could not have held
— the
rebuild paying off.

Tests

One test from #69 asserted the old refusal for "27 Havenwood Rise"; updated,
with its docstring recording that the assertion described the defect rather than
the intent. Two tests replace it for what is genuinely ambiguous: no exact
match among partial ones, and an exact number on two streets with no street to
discriminate.

Changes both paths identically — narrowing happens after the fetch, so live and
snapshot behave the same, which keeps the fallback honest.

./scripts/validate.sh2220 passed, 28 skipped.

…s not a match

Both sources match `paon` by SUBSTRING, so asking for "5 Alexandra Road" also
returns 15, 25 and 5A. Two separate faults followed, and the uniqueness check
solved neither.

**"5" was refused.** 15 came back with it, the check saw two buildings and
returned None -- though 5 was exactly one of the candidates and exactly what was
asked for. Measured across six outcodes of the real artifact: 14% of addresses,
one in seven, were unresolvable this way. In every case of that class the exact
match is present, because its presence is WHY the collision happened.

**"2" was answered with 25.** 25 was the only partial match, and a single
candidate passes a uniqueness check unchallenged. That is another property's
sale history returned as yours, with its price feeding
`subject_price_percentile` and `subject_vs_median_pct` -- a confident wrong
answer, which is the failure this module exists to prevent. Found by a test
written for the first fault; it had been reachable the whole time.

Narrowing to an exact `paon` before the uniqueness check fixes both, and is not
a relaxation: nothing is selected that the query did not already return, and the
check still runs afterwards on what survives. Where no candidate matches
exactly, every one of them merely contains the number asked for, so none is the
property the caller meant and None is right.

`_normalise_paon` compares on case and spacing only. Deliberately no more: "5"
and "5A" are different properties, and anything equating them would be inventing
identity rather than comparing it.

Verified against the real artifact:

    5 Alexandra Road   -> 3 sales, latest 2026-01-23 £225,000   (was None)
    9 Alexandra Road   -> 2 sales, latest 2022-12-05 £330,000   (was None)
    27 Havenwood Rise  -> 1 sale                                (was None)
    "Alexandra Road"   -> None      9999 Nowhere Street -> None

The 2026 sale matches the live answer retained from 2026-08-31; the 2008 and
2004 sales are history the eleven-year artifact could not have held.

One test from PR #69 asserted the old refusal for "27 Havenwood Rise" and is
updated, with its docstring recording that the assertion described the defect
rather than the intent. Two tests replace it for the cases that ARE ambiguous:
no exact match among partial ones, and an exact number on two streets with no
street given to discriminate.

This changes both paths identically -- the narrowing happens after the fetch, so
live and snapshot behave the same, which is what keeps the fallback honest.

./scripts/validate.sh: 2220 passed, 28 skipped.
@paulieb89
paulieb89 merged commit 8387c6d into main Sep 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant