Below "Abgelehnte Wohnungen", surface flats that survived the time
filter and aren't rejected but failed at least one of the user's
filters. Same collapsed-card style. Action buttons are replaced by
chips naming each failed dimension — "Zimmer", "Preis", "Größe",
"WBS", "Bezirk" — so it's obvious which constraint to relax.
Refactored matching: flat_matches_filter now delegates to a new
flat_filter_failures(flat, f) that returns the failed-dimension
labels (empty list = full match). rooms_min and rooms_max collapse
to a single "Zimmer" chip; reasons emit in stable _REASON_ORDER for
consistent rendering. The section is suppressed entirely when the
user has no filters set, since "everything matches" makes the chips
meaningless.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>