Dev diary: Mieter App week 17 — PLZ boundary cleanup
Week 17 was the Mieter App PLZ-boundary cleanup week. Twenty-three known boundary-straddling Postleitzahlen across Berlin, Hamburg, and München were each assigned an explicit Bezirk override in lib/mietspiegel/plz_overrides.dart. Test count moved from 98 to 117 with the new fixtures. All of it is Dart on Flutter 3.24, no plugins changed.
The bulk of the time went into reproducing the bug from the April review-mining cluster. A reviewer in 10627 Berlin flagged that the app put their flat in Charlottenburg when the Mietspiegel maps that street to Wilmersdorf. We pulled the actual Bundespost PLZ data, ran our resolver against every 5-digit PLZ in the three cities, and cross-referenced with the published 2025 Mietspiegel street-level appendix. Twenty-three PLZ codes resolved to a Bezirk that the Mietspiegel disagreed with. The fix is a small static lookup table that runs before PlzResolver.resolvePlz returns.
We also rewrote the Nebenkostenprüfung explanation strings in lib/nebenkosten/explanations.dart. Each of the seven flagged line items now ships with a one-sentence "why this looks wrong" plus a deep link to the relevant Mieterbund guide. The strings are pulled from a small JSON in assets/nebenkosten/explanations.de.json so they can be retranslated without touching code.
The interesting problem
Resolving a PLZ to a Bezirk is not a straight database lookup, because PLZ assignments are a postal-routing concept and Bezirk assignments are an administrative concept. They overlap mostly but not always. We considered three approaches: keep using the cheap PLZ-prefix heuristic and document the known-bad cases, ship a per-street override (precise, big bundle), or call out to a free geocoding API on first use and cache (no offline support). We picked the per-street override because the Mieter App ships fully offline, and the override table is 23 rows of JSON, not the 80 KB a per-street layer would have cost.
What we tried
We tried generating the override table automatically from a diff between Bundespost and the Mietspiegel street appendix, but the Mietspiegel appendix uses inconsistent street naming ("Bismarckstr." vs "Bismarckstraße") and the diff produced 200 false positives. We dropped the automation and curated the 23 rows by hand. Two hours of manual work; the alternative was two days of regex tuning that would still need a manual review pass.
The 117 integration tests now run in 4.1 seconds on a Pixel 4a, up from 3.7 seconds at 98 tests. The slowest test is the full PLZ corpus pass, which we already split into a separate suite that only runs in CI.
Next week
Next week is the Nebenkostenprüfung clarification work for review-mining theme 2 — the explanation strings ship in 1.4.2 and we want at least one Mieterbund deep link per flagged item before we cut the release on 2026-05-08. The Betriebskosten OCR scanner stays on its own branch until accuracy crosses 92 percent; current evaluation set sits at 87 percent.