feat(filter): Berlin-Bezirk filter in Einstellungen
Adds a collapsible 12-Bezirk checkbox list to the filter tab. The UI speaks Bezirke; internally the match runs on the PLZ extracted from flat.address and resolved to a dominant Bezirk via a curated 187-PLZ map (berlin_districts.py). - Migration 0011 adds user_filters.districts (CSV of selected names) - Empty stored value = no filter = all Bezirke ticked in the UI. Submitting "all ticked" or "none ticked" both normalise to empty so the defaults and the nuclear state mean the same thing. - When a Bezirk filter is active, flats with an unknown/unmapped PLZ are excluded — if the user bothered to narrow by district, sneaking in unplaceable flats would be the wrong default. - Filter summary on the Wohnungen page shows "N Bezirke" so it's visible the filter is active. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
abd614604f
commit
d13f9c5b6e
7 changed files with 202 additions and 20 deletions
120
web/berlin_districts.py
Normal file
120
web/berlin_districts.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"""Berlin-Bezirke taxonomy + PLZ lookup.
|
||||
|
||||
The user-visible taxonomy is the 12 Berlin Bezirke. We derive the Bezirk
|
||||
from the flat's freeform address by extracting the 5-digit PLZ and
|
||||
looking it up in PLZ_TO_DISTRICT.
|
||||
|
||||
Some PLZs straddle two Bezirke in reality. Each is mapped to its
|
||||
dominant Bezirk — close enough for a list filter. Unmapped PLZs / flats
|
||||
without a parseable address resolve to None; the filter treats those as
|
||||
"unknown" and excludes them when any Bezirk filter is active.
|
||||
"""
|
||||
import re
|
||||
|
||||
DISTRICTS: list[str] = [
|
||||
"Mitte",
|
||||
"Friedrichshain-Kreuzberg",
|
||||
"Pankow",
|
||||
"Charlottenburg-Wilmersdorf",
|
||||
"Spandau",
|
||||
"Steglitz-Zehlendorf",
|
||||
"Tempelhof-Schöneberg",
|
||||
"Neukölln",
|
||||
"Treptow-Köpenick",
|
||||
"Marzahn-Hellersdorf",
|
||||
"Lichtenberg",
|
||||
"Reinickendorf",
|
||||
]
|
||||
|
||||
_DISTRICT_PLZ: dict[str, tuple[str, ...]] = {
|
||||
"Mitte": (
|
||||
"10115", "10117", "10119", "10178", "10179",
|
||||
"10551", "10553", "10555", "10557", "10559",
|
||||
"13347", "13349", "13351", "13353", "13355", "13357", "13359",
|
||||
),
|
||||
"Friedrichshain-Kreuzberg": (
|
||||
"10243", "10245", "10247", "10249",
|
||||
"10961", "10963", "10965", "10967", "10969",
|
||||
"10997", "10999",
|
||||
),
|
||||
"Pankow": (
|
||||
"10405", "10407", "10409",
|
||||
"10435", "10437", "10439",
|
||||
"13086", "13088", "13089",
|
||||
"13125", "13127", "13129",
|
||||
"13156", "13158", "13159",
|
||||
"13187", "13189",
|
||||
),
|
||||
"Charlottenburg-Wilmersdorf": (
|
||||
"10585", "10587", "10589",
|
||||
"10623", "10625", "10627", "10629",
|
||||
"10707", "10709", "10711", "10713", "10715", "10717", "10719",
|
||||
"10789",
|
||||
"14050", "14052", "14053", "14055", "14057", "14059",
|
||||
"14193", "14195", "14197", "14199",
|
||||
),
|
||||
"Spandau": (
|
||||
"13581", "13583", "13585", "13587", "13589",
|
||||
"13591", "13593", "13595", "13597", "13599",
|
||||
"14089",
|
||||
),
|
||||
"Steglitz-Zehlendorf": (
|
||||
"12157", "12159", "12161", "12163", "12165", "12167", "12169",
|
||||
"12203", "12205", "12207", "12209",
|
||||
"12247", "12249",
|
||||
"14109", "14129",
|
||||
"14163", "14165", "14167", "14169",
|
||||
),
|
||||
"Tempelhof-Schöneberg": (
|
||||
"10777", "10779", "10781", "10783", "10785", "10787",
|
||||
"10823", "10825", "10827", "10829",
|
||||
"12099", "12101", "12103", "12105", "12107", "12109",
|
||||
"12277", "12279",
|
||||
"12305", "12307", "12309",
|
||||
),
|
||||
"Neukölln": (
|
||||
"12043", "12045", "12047", "12049",
|
||||
"12051", "12053", "12055", "12057", "12059",
|
||||
"12347", "12349",
|
||||
"12351", "12353", "12355", "12357", "12359",
|
||||
),
|
||||
"Treptow-Köpenick": (
|
||||
"12435", "12437", "12439",
|
||||
"12459", "12487", "12489",
|
||||
"12524", "12526", "12527",
|
||||
"12555", "12557", "12559",
|
||||
"12587", "12589",
|
||||
),
|
||||
"Marzahn-Hellersdorf": (
|
||||
"12619", "12621", "12623", "12627", "12629",
|
||||
"12679", "12681", "12683", "12685", "12687", "12689",
|
||||
),
|
||||
"Lichtenberg": (
|
||||
"10315", "10317", "10318",
|
||||
"10365", "10367", "10369",
|
||||
"13051", "13053", "13055", "13057", "13059",
|
||||
),
|
||||
"Reinickendorf": (
|
||||
"13403", "13405", "13407", "13409",
|
||||
"13435", "13437", "13439",
|
||||
"13465", "13467", "13469",
|
||||
"13503", "13505", "13507", "13509",
|
||||
),
|
||||
}
|
||||
|
||||
PLZ_TO_DISTRICT: dict[str, str] = {
|
||||
plz: district
|
||||
for district, plzs in _DISTRICT_PLZ.items()
|
||||
for plz in plzs
|
||||
}
|
||||
|
||||
_PLZ_RE = re.compile(r"\b(\d{5})\b")
|
||||
|
||||
|
||||
def district_for_address(address: str | None) -> str | None:
|
||||
if not address:
|
||||
return None
|
||||
m = _PLZ_RE.search(address)
|
||||
if not m:
|
||||
return None
|
||||
return PLZ_TO_DISTRICT.get(m.group(1))
|
||||
Loading…
Add table
Add a link
Reference in a new issue