feat(api): decide which nearby schools a page may offer

Hard filters encode claims the section may not make — a selective school is
not an alternative to a non-selective one, a special school is not comparable
to a mainstream one, a Girls school is not an option for a Boys school's
reader — so they never relax. Soft preferences describe closeness of fit, so
they relax across three tiers, and only far enough to reach three; the
remaining slots up to six fill from the tiers already opened.

PHASE_GROUPS moves to schemas.py so this module can share it without
importing app, which would be a cycle.

_mask() exists because Series.apply on an empty Series returns a DataFrame,
and using that as a mask drops every column — so the next lookup raises
KeyError instead of yielding no rows. A special school with no special school
near it empties the frame at the provision filter, which is the ordinary case
for most special schools, so this was a crash on a common path rather than an
edge case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
TudorandClaude Opus 5 committed 2026-09-21 22:40:10 +01:00
1 parent 8a23e3657d
commit b571d9c549
4 files changed
+508 -9

No files matched your search

+12
View File
@@ -532,6 +532,18 @@ RANKING_COLUMNS = [
"gcse_grade_91_pct",
]
# Maps user-facing phase filter values to the GIAS PhaseOfEducation values they
# include. All-through schools appear in both primary and secondary results,
# which is why this is a set per phase rather than a single string comparison.
#
# Lives here rather than in app.py because similar_schools.py needs it too, and
# importing app from there would be a cycle.
PHASE_GROUPS: dict[str, set[str]] = {
"primary": {"primary", "middle deemed primary", "all-through"},
"secondary": {"secondary", "middle deemed secondary", "all-through", "16 plus"},
"all-through": {"all-through"},
}
# School listing columns
SCHOOL_COLUMNS = [
"urn",