fix: order nearby schools by distance, not by how alike they are

Reported from staging: a Catholic primary showed six Catholic primaries, none
of them close enough to be a real option, and omitted the community school
down the road.

Three causes, compounding. Ranking put tier before distance, so a faith match
at 2.9 miles outranked a community school at 0.3. The ENOUGH=3 stopping rule —
added so a cap of six would not drag in weak distant matches — filled the row
from the best tier before it ever widened, which is what made every card
Catholic. And a 3-mile tier-1 radius is sane for a secondary and most of a city
for a primary, whose catchments are routinely under a mile.

The premise was backwards. For a parent, distance is a constraint and intake is
a preference; a school beyond a primary catchment is not a weaker option, it is
not an option. So distance now decides the order and nothing else does. The
hard filters are untouched — they were always where the defensibility lived.
Similarity survives as chips on the card: reported, so a reader applies their
own weighting, rather than ranked, so we apply ours for them.

Reach is capped per phase (primary 2, secondary 6, post-16 10) as a sanity
bound, not a target: ordering already handles density, so the cap only decides
what happens where an area is sparse. A primary with nothing inside two miles
now renders no section, which is the honest answer.

Deleted: the tier system, the stopping rule, the tier-dependent lede, the
`tier` field, the tier-3 fallback chip and its style. select_similar also stops
taking is_secondary — it reads the phase from the subject's own row, so no
caller can hand it one that disagrees with the data.

The heading is now "Other schools nearby". The hard filters still guarantee a
comparable set, but nothing ranks on likeness, so the heading no longer says it
does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
TudorandClaude Opus 5 committed 2026-09-22 13:06:32 +01:00
1 parent 151cf4bc80
commit 5c0ccc693d
11 files changed
+343 -278

No files matched your search

+11 -14
View File
@@ -41,7 +41,7 @@ from .data_loader import get_data_info as get_db_info
from . import flags
from .places import build_place_index, build_place_registry, places_for_urn
from .schemas import METRIC_DEFINITIONS, PHASE_GROUPS, RANKING_COLUMNS, SCHOOL_COLUMNS
from .similar_schools import is_secondary_phase, select_similar
from .similar_schools import select_similar
from .utils import clean_for_json, convert_to_native
# Values to exclude from filter dropdowns (empty strings, non-applicable labels)
@@ -266,20 +266,18 @@ def _places_payload(urn: int) -> list[dict]:
return payload
def _similar_schools_payload(urn: int, phase: str | None) -> list[dict]:
"""Nearby schools this page may offer as alternatives.
def _similar_schools_payload(urn: int) -> list[dict]:
"""The nearest eligible schools this page may offer, closest first.
Phase and reach are read from the school's own row inside select_similar,
so nothing here can hand it a phase that disagrees with the data.
Wrapped: a failure in selection must never 500 a page that is otherwise
complete, which is the posture get_supplementary_data already takes. The
section simply does not render.
"""
try:
# Decided in similar_schools, beside the PHASE_GROUPS bucket it selects
# from, so the two cannot drift. A substring test for "secondary" here
# would miss "16 plus" and hand a sixth-form college the primary bucket.
return select_similar(
load_latest_school_data(), int(urn), is_secondary_phase(phase)
)
return select_similar(load_latest_school_data(), int(urn))
except Exception:
import logging
@@ -999,11 +997,10 @@ async def get_school_details(request: Request, urn: int):
# and authority both fall below the publish threshold has nowhere to
# point, and the page renders without the module.
"places": _places_payload(urn),
# Nearby schools of the same phase and a comparable intake. Always
# present on a build with this code; the frontend treats absent and
# empty identically, which is what lets the two images deploy
# independently.
"similar_schools": _similar_schools_payload(urn, latest.get("phase")),
# The nearest eligible schools, closest first. Always present on a
# build with this code; the frontend treats absent and empty
# identically, which is what lets the two images deploy independently.
"similar_schools": _similar_schools_payload(urn),
"yearly_data": clean_for_json(school_data),
# Supplementary data (null if not yet populated by Kestra)
"ofsted": supplementary.get("ofsted"),