Neighbourhood Fit Score
Een score-engine voor wijken, gemaakt voor EU-expats die naar Brussel verhuizen — 724 statistische sectoren, 3 persona-scenario’s, routing over het echte voetgangersnetwerk, en de laag die geen enkele concurrent ooit heeft uitgebracht: concrete verbetersuggesties die precies tonen welke ene voorziening de score van een sector zou doen stijgen, en met hoeveel punten
- Probleem — 37,2% van de 1,25 miljoen Brusselaars is niet-Belg en kiest uit 19 gemeenten met tools die een cijfer geven, maar geen reden
- Inzicht — Concurrenten strijden om wát de score is; het open terrein is voor wie, waarom en hoe je hem verbetert — geen enkele EU-tool raakt het aan
- Oplossing — Ik bouwde solo een score-engine over 724 sectoren × 3 persona’s: ze legt het waarom uit, stelt verbeteringen voor en vergelijkt twee sectoren
- Resultaat — MVP live in publieke bèta op cityscore.ontwrpn.com, eerste 20 gebruikers, 2.172 voorberekende scores, <5 ms API, €0 LLM-kosten
Een stad vol nieuwkomers zonder bruikbare locatie-intelligentie
Brussel is in de praktijk een stad vol nieuwkomers: 37,2% van de 1,25 miljoen inwoners is niet-Belg, aangetrokken door de EU-instellingen, de NAVO, internationale ngo’s en een groeiende diaspora van remote workers. Alleen al de Europese Commissie stelt 21.705 mensen tewerk, en elke maart en oktober komen er zo’n 1.900 nieuwe Blue Book-stagiaires bij. Het gaat om hoogopgeleide, goedverdienende nieuwkomers die standaard Engels spreken en een beslissing moeten nemen met bitter weinig informatie: welke van de 19 gemeenten past bij hún specifieke levenssituatie — een gezin met jonge kinderen, een remote worker die cafés zoekt, een senior die nood heeft aan frequent openbaar vervoer en een huisarts in de buurt? Bestaande tools geven geen antwoord op die vraag. Ze geven een cijfer.
Een concurrentieaudit van 8 tools (Walk Score, Niche, AreaVibes, Local Logic, Rightmove, Novad, mylocationscore, AARP Livability Index) legde drie structurele lacunes bloot: elke noemenswaardige concurrent richt zich op de VS of Canada; scores bestaan zonder uitleg — een 72/100 zegt je niets over wat die score bepaalde; en de ‘hoe verbeter je het’-laag is helemaal niet ingevuld. Geen enkele concurrent heeft ooit ‘voeg één apotheek toe op deze plek → score stijgt met 9 punten’ uitgebracht.
Concurrenten strijden om wát de score is. Het open terrein is voor wie, waarom en hoe je hem verbetert — vier werkwoorden die de EU-markt voor locatie-intelligentie volledig onbeantwoord liet
Open data, echte straten — geen afstand in vogelvlucht
De score-engine bouwt voort op vier open databronnen: 724 statistische sectoren van Statbel (elk ~100–200 inwoners, de Belgische censuseenheid), het OpenStreetMap Geofabrik Belgium PBF-extract over 18 voorzieningscategorieën, de STIB/MIVB GTFS-feeds met op frequentie gewogen haltes, en de UrbIS GIS-lagen van Brussel. De belangrijkste beslissing was routing over echte voetgangersstraten via een OSMnx walk graph (92.950 knooppunten, 264.936 verbindingen) in plaats van afstand in vogelvlucht — die de bereikbaarheid stelselmatig met zo’n 20–40% overschat.
De afstandsdecay werkt met een plateau+Gaussiaanse functie: volle punten binnen een drempelreistijd, een vloeiende afname naar nul aan het maximum, zonder scherpe rand. De transit-subscores vermenigvuldigen die decay met een frequentiefactor — een halte om de 5 minuten haalt de volle score; een halte om de 30 minuten scoort evenredig lager, maar houdt 40% van haar decay over in plaats van niets. De score-engine draaide zo’n 10 minuten op een laptop om alle 2.172 scores voor te berekenen (724 sectoren × 3 scenario’s). De API-antwoorden tijdens runtime duren <5 ms — een opzoeking, geen berekening.
MapLibre GL-choropleth over OpenFreeMap (zonder sleutel). De gele markers tonen waar één nieuwe voorziening de grootste scorewinst oplevert
Verklaren, wisselen, verbeteren, vergelijken — de vier werkwoorden die concurrenten oversloegen
Bij elke score hoort een opdeling per categorie, kleurgecodeerde sterke punten en hiaten, en — de onderscheidende laag — concrete verbetersuggesties. Voor elke sector × scenario zoekt de engine de top 3 categorieën waar één extra voorziening de grootste scorewinst oplevert, met een kaartmarker op de voorgestelde plek. Output: ‘+1 apotheek binnen 400 m → Seniorscore 62 → 71 (+9 punten).’ Bij de lancering verschenen drie persona-scenario’s — Gezin, Senior, Remote Work — met de scenario-gewichten zichtbaar in de UI, als bewuste keuze voor transparantie: ze nodigen uit tot kritisch nakijken in plaats van zich te verschuilen achter een black box.
Het ‘Vraag’-paneel streamt een antwoord van 2 à 4 zinnen dat enkel steunt op de eigen data van de sector, via Groq (Llama 3.3 70B, gratis tier) over server-sent events — met de instructie om geen externe kennis te gebruiken en demografie te vermijden. Het Vergelijk-paneel zet twee sectoren naast elkaar met de delta per categorie, een winnaar per rij en een verdict in gewone mensentaal, onderbouwd door /api/compare. Met het filter ‘Zoek op voorziening’ selecteren gebruikers meerdere voorzieningschips (hondenweides incluis) en stellen ze een minimumscore in om de passende sectoren te laten oplichten.
Scorepaneel voor Vieille Halle aux Blés (scenario Gezin, score 86). Alles, van de ring tot de Score Boosters, wordt in <5 ms uit voorberekende data gegenereerd

Live bèta — zoek om het even welk Brussels adres, kies een scenario en verken verbeteringen
De zwaarste beperking was zware geo-compute draaien binnen een krap hostingbudget
Het centrale architectuurinzicht: scheid de offline berekening van het serveren tijdens runtime. Zware geo-bibliotheken (osmnx, geopandas, NetworkX Dijkstra) krijg je niet aan de praat in een productiewebserver binnen het plafond van 512 MB RAM van Render’s gratis tier. De oplossing is een strikte splitsing in twee lagen: een offline pipeline (OSMnx, geopandas, NetworkX, Statbel, STIB GTFS) draait lokaal of maandelijks in GitHub Actions om de data te verversen, serialiseert 2.172 scores naar CSV en commit die naar de repo. De runtime-API (FastAPI + shapely + numpy + SQLModel) seedt vanuit die CSV bij het opstarten van de container en beantwoordt verzoeken met eenvoudige opzoekingen.
Belangrijke technische keuzes: Python FastAPI boven Node Express (geo-bibliotheken zijn first-class in Python; één taal voor pipeline én runtime); shapely punt-in-polygoon boven PostGIS (724 sectoren passen volledig in het geheugen — geen Postgres-extensie nodig); statistische sectoren van Statbel boven UrbIS-wijken (fijnere granulariteit, verdedigbaar tegen het MAUP); OpenFreeMap boven een commerciële basiskaart (geen API-sleutel, geen rate limits, EU-gehost, nul kosten); Nominatim-geocoding met DB-cache (de limiet van 1 req/s opgevangen door een agressieve SQLite-cache). In juni 2026 verhuisde de app weg van Render + Supabase naar een zelf gehoste Coolify-instantie op Hetzner met interne Postgres, waarmee de Render Postgres-vervaltermijn van 30 dagen verdween.
pyrosm was de verkeerde gok qua dependency — incompatibel met Python 3.14 door een kapotte pyrobuf-build. Overstappen naar de osmium-tool CLI (C++, geen afhankelijkheid van een Python-versie) kostte twee dagen. Les: check de compatibiliteit van je geo-dependencies vóór je je vastlegt op een parse-strategie
Productie-hardening, toegankelijkheid en design system in één sprint
De hardening-sprint van juni 2026 omvatte: gzip + ETag/Cache-Control + een choropleth die in het proces gecachet wordt; afronding van coördinaten (−66% payload); slowapi rate limiting op /api/explain en /api/score; CSP + security headers; validatie van scenario en categorie (422/400); geen lekkende foutstrings; en een nieuw /api/weights-endpoint als single source of truth, dat een scenario-gewichtentabel schrapte die gedupliceerd zat over de pipeline-config, het seed-script en de frontend. MapLibre splitste ik af in een lang gecachete vendor-chunk.
De WCAG 2.2 AA-toegankelijkheidsronde, geverifieerd met axe-core (0 schendingen): listbox met toetsenbordbediening + een sectorkiezer buiten de kaart, foutmeldingen met role=alert, focus-visible, prefers-reduced-motion, aanraakdoelen van ≥24 px en responsieve reflow bij 320 px. Visuele afwerking — getrapte reveals, een rAF-score die optelt, gradiëntbalken en -ring — alles met respect voor reduced-motion. De design-system-ronde verving zo’n 20 ad-hoc lettergroottes en ongelijke sectiepadding door één typeschaal en een spacingschaal van 4 px. Twee sluimerende bugs opgelost: een SQLite-migratiecrash en dubbel geseede kaart-POI’s.
MVP gelanceerd, 20 bètagebruikers, eerlijk over wat een v1 voor één stad mag claimen
MVP gelanceerd als publieke bèta op cityscore.ontwrpn.com. De cijfers bij de lancering: 724 Brusselse statistische sectoren gescoord, 3 persona-scenario’s, 2.172 voorberekende scores, 4.533 verbetersuggesties (tot 3 per sector × scenario), API-responstijd <5 ms, totale DB-footprint <10 MB, €0 hosting op een gedeelde, zelf gehoste Hetzner-VPS. De eerste 20 bètagebruikers wierf ik via Brusselse expatkanalen. De multi-stadinfrastructuur voor Antwerpen staat al klaar (per stad parametriseerbare pipeline, migratie van de city-kolom) — de data-pipeline-run moet enkel nog gebeuren.
Wat ik anders zou aanpakken: starten met de verbeterlaag. Het is de sterkste differentiator, en toch bouwde ik ze pas in week 5. Voor toekomstige projecten: begin met datgene wat niemand anders doet en valideer het eerst. De pivot van pyrosm naar osmium kostte twee dagen en had ik kunnen vermijden met een compatibiliteitscheck van 30 minuten. De vervaltermijn van 30 dagen op Render Postgres (geen 90, zoals ik aannam) bevestigde de les: test altijd je herstel, niet alleen je deploy — en net daarom staan alle 2.172 scores als seed-data in de repo.
De Brusselse choropleth met het scorepaneel open. Het product staat live op cityscore.ontwrpn.com
Bronnen
- Boeing (2019). Street network models and indicators for every urban area in the world; pedestrian circuity typically ≈ 1.18–1.40.
- Giacomin & Levinson (2015). Road network circuity in metropolitan areas. Environment and Planning B 42(6):1040–1053. — echte looproutes zijn ~1,2–1,4× de hemelsbrede afstand
