303.525 datasets geïndexeerd1.419 uitgevers
Open Data IndexDe index van open data over Nederland
Open Data Index›Bouwen›Schoolkeuzehulp: basisscholen rond je adres, met leerlingen en schooladviezen
ToolOm te beginnenOnderwijsWonen en ruimte

Schoolkeuzehulp: basisscholen rond je adres, met leerlingen en schooladviezen

Typ een postcode en huisnummer en zie alle basisscholen binnen een straal, op afstand gesorteerd, met hun leerlingenaantal door de jaren en welk deel van de kinderen een havo- of vwo-advies kreeg. Alleen open data van DUO en de BAG.

Je AI-assistentOpen Data Index MCP aan
De MCP-server van Open Data Index staat aan in je chat. Zo leest de assistent dit project en de bronnen zelf.
Jij plakt de prompt van dit project
De assistent haalt op bij Open Data Index
  • geef_project("schoolkeuzehulp")
  • geef_bron("duo-onderwijsdata")
  • geef_bron("pdok-bag")
De assistent

Ik heb het project, de geteste aanroepen en de valkuilen. Ik bouw een tool met DUO open onderwijsdata en Basisregistratie Adressen en Gebouwen (BAG). Zo kan het eruitzien:

Dit bouw je zelf. Kopieer de prompt naar Claude, ChatGPT of Cursor, met de MCP-server aan, of volg de stappen hieronder.

Volg de stappen
8:15
Om de hoek
Scholen in de buurt
Stadsplateau 1, Utrechtwijzig
≤ 1,5 kmBasisonderwijsDichtstbij ↓
13 scholenbinnen 1,5 km
0500 m1 km1,5 km
Kinderen 2015 → 2025 · advies peiljaar 2024
Kindercampus Molenpark
616 m182 → 152 kinderen −16%
Advies havo of hoger 59–73%meest: vwo
KBS De Pijlstaart
701 m146 → 191 kinderen +31%
Advies havo of hoger 41–81%meest: havo/vwo
KBS De Catharijnepoort
823 m324 → 157 kinderen −52%
Advies havo of hoger 52–81%meest: havo/vwo
OBS De Kleine Dichter
1.124 m186 → 341 kinderen +83%
Advies havo of hoger 17–57%meest: vmbo-gt/havo
ZoekenKaartBewaardVergelijk
Mockup

Dit is een mockup, geen werkende versie, met de cijfers uit onze proefdraai van 16 oktober 2026 voor Stadsplateau 1 in Utrecht (DUO: leerlingen op 1 oktober 2015 en 2025, schooladviezen peiljaar 2024). De MCP-server zet je aan met het adres op Voor AI.

Voor wie

Ouders die een basisschool zoeken en verder willen kijken dan de website van de school. Lokale journalisten die willen laten zien welke scholen groeien of krimpen. Gemeenten en wijkteams die het onderwijsaanbod in een wijk in beeld willen brengen. En iedereen die een eerste kaart-app met echte overheidsdata wil bouwen.

Wat je krijgt

Een script (en daarna een kleine webapp) dat een adres omzet in een punt, alle 6.088 vestigingen in het basisonderwijs van een coördinaat voorziet en de scholen binnen een straal toont met afstand, leerlingen in 2015 en 2025 en het aandeel havo- en vwo-adviezen in het nieuwste peiljaar.

Proefdraai op 16 oktober 2026 voor het Stadskantoor in Utrecht (Stadsplateau 1, 3521 AZ): 13 basisscholen binnen 1.500 meter. De dichtstbijzijnde is Kindercampus Molenpark op 616 meter (182 leerlingen in 2015, 152 in 2025). De Vrije School Utrecht groeide het hardst, van 325 naar 437 leerlingen; KBS De Catharijnepoort halveerde bijna, van 324 naar 157. Bij ODBS Pieterskerkhof kreeg in peiljaar 2024 elk kind met een advies havo of hoger.

Landelijk zaten er op 1 oktober 2025 1.356.909 kinderen op 6.096 vestigingen in het basisonderwijs, tegen 1.443.066 op 6.584 vestigingen in 2015 (6% minder kinderen). In het adviesbestand van peiljaar 2024 staat bij 30.235 van de 43.860 cellen een -1 (1 tot 4 leerlingen): daarom toont het script per school een onder- en bovengrens, geen schijnprecisie.

Leerlingen in het basisonderwijs: 1,36 miljoen in 2025/26, 10% minder dan in 1996/97

Leerlingen in het basisonderwijs, 1996/97 – 2025/26, in miljoen. Het hoogste punt was 2008/09 (1,55).

Bron: DUO, Historische leerlingentellingen PO gemeente per jaar · opgehaald 25 oktober 2026 · selectie: onderwijssoort BO; peildatum 1 oktoberOpen de tabel Los blok

Zo zou je kunnen starten7 stappen

  1. 1

    Zet het adres om in een punt via de BAG

    Gebruik de BAG-WFS van PDOK met een filter op postcode en huisnummer, laag bag:verblijfsobject. Vraag srsName=EPSG:4326, dan krijg je lengte en breedte in graden (in GeoJSON eerst de lengte). Gebruik niet de PDOK Locatieserver: robots.txt van api.pdok.nl verbiedt /bzk/locatieserver/*. Valkuil: één huisnummer kan meerdere verblijfsobjecten hebben (hier 1 en 1T). Kies het object zonder huisletter en toevoeging, of laat de gebruiker kiezen. Het filter is FES 2.0 XML in de parameter filter; een postcode zonder spatie en in hoofdletters (3521AZ). Het antwoord kwam binnen een seconde.

    https://service.pdok.nl/lv/bag/wfs/v2_0?service=WFS&version=2.0.0&request=GetFeature&typeNames=bag:verblijfsobject&outputFormat=application/json&srsName=EPSG:4326&filter=<fes%3AFilter xmlns%3Afes%3D"http%3A%2F%2Fwww.opengis.net%2Ffes%2F2.0"><fes%3AAnd><fes%3APropertyIsEqualTo><fes%3AValueReference>postcode<%2Ffes%3AValueReference><fes%3ALiteral>3521AZ<%2Ffes%3ALiteral><%2Ffes%3APropertyIsEqualTo><fes%3APropertyIsEqualTo><fes%3AValueReference>huisnummer<%2Ffes%3AValueReference><fes%3ALiteral>1<%2Ffes%3ALiteral><%2Ffes%3APropertyIsEqualTo><%2Ffes%3AAnd><%2Ffes%3AFilter>
    {
    "type": "FeatureCollection",
    "numberMatched": 2,
    "name": "verblijfsobject",
    "features": [
    { "type": "Feature", "id": "verblijfsobject.be5cbd3b-…", "properties": { "identificatie": "0344010000163716", …, "openbare_ruimte": "Stadsplateau", "huisnummer": 1, "huisletter": "T", "toevoeging": "", "postcode": "3521AZ", "woonplaats": "Utrecht", "bouwjaar": 2014, … }, "geometry": { "type": "Point", "coordinates": [ 5.1078212966518013, 52.089758966081369 ] } },
    { "type": "Feature", "id": "verblijfsobject.9e217dfe-…", "properties": { "identificatie": "0344010000163715", "oppervlakte": 64446, …, "huisnummer": 1, "huisletter": "", "toevoeging": "", "postcode": "3521AZ", "woonplaats": "Utrecht", "bouwjaar": 2014, … }, "geometry": { "type": "Point", "coordinates": [ 5.1077492228137116, 52.089601288453508 ] } }
    ]
  2. 2

    Haal de scholen op uit het adressenbestand van DUO

    DUO publiceert alle vestigingen in het basisonderwijs (6.088 op 16 oktober 2026) met naam, adres, gemeente en denominatie, in de CKAN-datastore van onderwijsdata.duo.nl. Filter op GEMEENTENUMMER (tekst met voorloopnul: "0344" voor Utrecht, 97 vestigingen) of haal ze in één keer allemaal op met limit=100000. Dat laatste is beter: wie bij een gemeentegrens woont, wil ook de school net over de grens zien. Let op de veldnaam met streepje en spatie (HUISNUMMER-TOEVOEGING, "22-A", "26-28"): het huisnummer moet je er zelf uit halen. Zonder limit geeft de datastore maar 1.000 rijen.

    https://onderwijsdata.duo.nl/api/3/action/datastore_search?resource_id=dcc9c9a5-6d01-410b-967f-810557588ba4&filters={"GEMEENTENUMMER"%3A"0344"}&fields=INSTELLINGSCODE,VESTIGINGSCODE,VESTIGINGSNAAM,STRAATNAAM,HUISNUMMER-TOEVOEGING,POSTCODE,DENOMINATIE&limit=2
    "records": [
        {
            "INSTELLINGSCODE": "28BF",
            "VESTIGINGSCODE": "28BF00",
            "VESTIGINGSNAAM": "School v/d Stichting Hindoe Onderwijs Shri Krishna school",
            "STRAATNAAM": "Van Lawick van Pabstlaan",
            "HUISNUMMER-TOEVOEGING": "3",
            "POSTCODE": "3453RA",
            "DENOMINATIE": "Hindoeïstisch"
        },
        …
    ],
    … "total": 97
  3. 3

    Geef elke school een coördinaat uit RIO

    Het adressenbestand heeft geen coördinaten, maar de Registratie Instellingen en Opleidingen (RIO) wel: de tabel onderwijslocaties (13.646 rijen) heeft GPS_LATITUDE, GPS_LONGITUDE en het BAG-id. Koppel op postcode plus huisnummer; dat lukte voor 5.836 van de 6.088 vestigingen. Je hoeft dus niet 6.088 keer de BAG te bevragen. Een filter mag een lijst zijn: {"POSTCODE": ["3521EX", "9306TC"]}.

    https://onderwijsdata.duo.nl/api/3/action/datastore_search?resource_id=a7e3f323-6e46-4dca-a834-369d9d520aa8&filters={"POSTCODE"%3A"3521EX"}
    "records": [
        {
            "_id": 5856,
            "ONDERWIJSLOCATIECODE": "106X637",
            "BAG_ID": "0344010000097484",
            "GPS_LATITUDE": 52.07973474,
            "GPS_LONGITUDE": 5.11133012,
            "STRAATNAAM": "Heycopplein",
            "HUISNUMMER": 1,
            "HUISNUMMERTOEVOEGING": null,
            "POSTCODE": "3521EX",
            "PLAATSNAAM": "Utrecht"
        }
    ],
  4. 4

    Vang de rest op via de RIO-relatie vestiging naar locatie

    Voor de vestigingen zonder adresmatch is er de tabel relaties_onderwijslocatiegebruiken_vestigingserkenningen: VESTIGINGSCODE naar ONDERWIJSLOCATIECODE, met begin- en einddatum. Neem alleen rijen zonder EINDDATUM: De Kleine Dichter (17RK00) verhuisde op 1 augustus 2025 van locatie 106X370 naar 106X637. 987 vestigingen hebben meer dan één actieve locatie (dependances, gebouwen van meerdere besturen); kies dan het hoofdadres, of toon alle gebouwen. Met adres en relatie samen bleven 11 van de 6.088 vestigingen zonder coördinaat over.

    https://onderwijsdata.duo.nl/api/3/action/datastore_search?resource_id=c18ec7dd-aa4e-4b51-997c-782955f1aa38&filters={"VESTIGINGSCODE"%3A"17RK00"}
    "records": [
        {
            "_id": 6204,
            "ONDERWIJSLOCATIECODE": "106X370",
            "ONDERWIJSBESTUURID": "106B332",
            "VESTIGINGSCODE": "17RK00",
            "BEGINDATUM": "2021-08-01T00:00:00",
            "EINDDATUM": "2025-08-01T00:00:00"
        },
        {
            "_id": 6501,
            "ONDERWIJSLOCATIECODE": "106X637",
            "ONDERWIJSBESTUURID": "106B332",
            "VESTIGINGSCODE": "17RK00",
            "BEGINDATUM": "2025-08-01T00:00:00",
            "EINDDATUM": null
        }
    ],
  5. 5

    Leerlingen per schooljaar: groeit of krimpt de school?

    De historische leerlingentellingen (229.583 rijen, 1996 tot en met 2025) geven per vestiging en per schooljaar het aantal leerlingen op 1 oktober. SCHOOLJAAR is het jaar waarin het schooljaar begint: 2025 is 2025/26. Valkuil: fusies en verhuizingen. De Kleine Dichter telde in 2024 125 leerlingen op 17RK00 en 345 op 17RK01; in 2025 bestaat alleen 17RK00 nog, met 341. Kijk dus naar alle vestigingen van dezelfde INSTELLINGSCODE voor je 'verdrievoudigd' schrijft. Optellen per instelling werkt ook niet altijd: bij Pieterskerkhof (17FL) telt 17FL01 (KC Cartesius) in 2025 nog 28 leerlingen mee, maar staat die vestiging niet meer in het adressenbestand.

    https://onderwijsdata.duo.nl/api/3/action/datastore_search?resource_id=0e7c3205-761b-41cb-9149-30576619a944&filters={"VESTIGINGSCODE"%3A"17RK00"}&fields=VESTIGINGSCODE,NAAM_VESTIGING,SCHOOLJAAR,AANTAL_LEERLINGEN&sort=SCHOOLJAAR desc&limit=3
    "records": [
        {
            "VESTIGINGSCODE": "17RK00",
            "NAAM_VESTIGING": "OBS De Kleine Dichter",
            "SCHOOLJAAR": 2025,
            "AANTAL_LEERLINGEN": 341
        },
        {
            "VESTIGINGSCODE": "17RK00",
            "NAAM_VESTIGING": "OBS De Kleine Dichter",
            "SCHOOLJAAR": 2024,
            "AANTAL_LEERLINGEN": 125
        },
        …
    ],
  6. 6

    Schooladviezen per school, met de afgeschermde cellen

    Het bestand 'Leerlingen bo en sbo per vestiging naar advies' geeft per vestiging, peiljaar en advies het aantal leerlingen. Het nieuwste peiljaar in de API was 2024 (bijgewerkt 9 januari 2026). Drie valkuilen. Eén: VESTIGINGSCODE is hier maar twee tekens ("00", "01"): plak de INSTELLINGSCODE ervoor om te koppelen aan de andere bestanden ("17RK" + "00"). Twee: ADVIES is een getal 1 tot 12 zonder labels in de API. De toelichting van DUO (toelichting-schooladviezen.pdf) noemt de adviezen in de volgorde van de ROD-codes: vso, pro, vmbo-b, vmbo-b/k, vmbo-k, vmbo-k/gt, vmbo-gt, vmbo-gt/havo, havo, havo/vwo, vwo en advies niet mogelijk. Dat 1 = vso en 2 = pro is, klopt met het speciaal basisonderwijs (2024: 2.618 keer code 2, 430 keer code 1), maar DUO zet de nummers zelf nergens bij. Drie: -1 betekent 1 tot 4 leerlingen. Op deze school zijn 7 van de 13 cellen afgeschermd. Reken met een onder- en bovengrens.

    https://onderwijsdata.duo.nl/api/3/action/datastore_search?resource_id=454c5dd4-6815-4b52-ae2f-b9eebc6afa6d&filters={"INSTELLINGSCODE"%3A"17RK"%2C"PEILJAAR"%3A2024}&fields=INSTELLINGSCODE,VESTIGINGSCODE,PEILJAAR,ADVIES,AANTAL_LEERLINGEN&sort=ADVIES
    total 13
    {"INSTELLINGSCODE": "17RK", "VESTIGINGSCODE": "01", "PEILJAAR": 2024, "ADVIES": 4, "AANTAL_LEERLINGEN": -1}
    {"INSTELLINGSCODE": "17RK", "VESTIGINGSCODE": "01", "PEILJAAR": 2024, "ADVIES": 6, "AANTAL_LEERLINGEN": 7}
    {"INSTELLINGSCODE": "17RK", "VESTIGINGSCODE": "00", "PEILJAAR": 2024, "ADVIES": 8, "AANTAL_LEERLINGEN": 7}
    {"INSTELLINGSCODE": "17RK", "VESTIGINGSCODE": "01", "PEILJAAR": 2024, "ADVIES": 8, "AANTAL_LEERLINGEN": 9}
    {"INSTELLINGSCODE": "17RK", "VESTIGINGSCODE": "01", "PEILJAAR": 2024, "ADVIES": 10, "AANTAL_LEERLINGEN": 17}
    {"INSTELLINGSCODE": "17RK", "VESTIGINGSCODE": "01", "PEILJAAR": 2024, "ADVIES": 11, "AANTAL_LEERLINGEN": 11}
    {"INSTELLINGSCODE": "17RK", "VESTIGINGSCODE": "00", "PEILJAAR": 2024, "ADVIES": 11, "AANTAL_LEERLINGEN": -1}
    …
  7. 7

    Kijk vooruit met de prognose per school

    DUO rekent per vestiging een leerlingenprognose door tot 2045 (bestand 'Prognoses bo en sbo per vestiging'). Handig voor ouders die willen weten of een school vol raakt, en voor een rij 'verwacht in 2030' naast de echte telling. Ook hier is VESTIGINGSCODE twee tekens en is de prognose een kommagetal: rond af bij het tonen. De Kleine Dichter: 341 leerlingen in 2025, verwacht 377 in 2026 en 407 in 2030, daarna dalend naar 285 in 2035.

    https://onderwijsdata.duo.nl/api/3/action/datastore_search?resource_id=f27c0d5c-59a0-41b9-b23f-f3a7ee03d4ba&filters={"INSTELLINGSCODE"%3A"17RK"%2C"JAAR"%3A[2026%2C2030%2C2035]}&sort=JAAR
    "records": [
        {
            "_id": 92581,
            "INSTELLINGSCODE": "17RK",
            "VESTIGINGSCODE": "00",
            "TYPE_PO": "BO",
            "JAAR": 2026,
            "PROGNOSEAANTALLEN": 377.08
        },
        {
            …
            "JAAR": 2030,
            "PROGNOSEAANTALLEN": 406.58
        },
        {
            …
            "JAAR": 2035,
            "PROGNOSEAANTALLEN": 285.36
        }
    ],

Hoe de bronnen samenkomen

  • Basisregistratie Adressen en Gebouwen (BAG)DUO open onderwijsdatavia coördinaat (lengte, breedte in EPSG:4326) en afstand

    De BAG levert het punt van het adres van de gebruiker, RIO levert de punten van de scholen. De afstand reken je zelf uit met de haversine-formule (in meters). Wil je in RD rekenen (EPSG:28992, meters, gewoon Pythagoras), vraag de BAG dan zonder srsName en zet de GPS-punten van RIO om. Hemelsbreed is niet looproute: voor een echte loop- of fietsafstand heb je een routeplanner nodig.

  • DUO open onderwijsdataBasisregistratie Adressen en Gebouwen (BAG)via BAG_ID van de onderwijslocatie (RIO) naar verblijfsobject

    Elke onderwijslocatie in RIO heeft een BAG_ID (bijvoorbeeld 0344010000097484 voor De Kleine Dichter). Daarmee haal je in de BAG het bouwjaar en de oppervlakte van het schoolgebouw op, bijvoorbeeld om m² per leerling te tonen.

  • DUO open onderwijsdataDUO open onderwijsdatavia INSTELLINGSCODE + VESTIGINGSCODE ("17RK" + "00" = "17RK00")

    Het adressenbestand en de leerlingentellingen gebruiken de volle code van zes tekens, het advies- en het prognosebestand alleen de laatste twee. Maak overal één sleutel van zes tekens voor je koppelt.

Laat AI het bouwen

Plak deze prompt in Claude, ChatGPT, Cursor of Lovable. Hij noemt de echte endpoints, de koppelsleutels en de valkuilen, zodat de assistent meteen werkende code schrijft. Controleer de uitkomst altijd met de bron zelf.

Bouw een kleine webapp (één HTML-bestand met JavaScript, of Next.js als je dat liever hebt) waarin ik een postcode en huisnummer intyp en een lijst en kaart krijg van alle basisscholen binnen een straal die ik kies (standaard 1.500 meter).

Data, allemaal open, zonder sleutel:
1. Adres naar punt: BAG-WFS van PDOK, https://service.pdok.nl/lv/bag/wfs/v2_0 met service=WFS&version=2.0.0&request=GetFeature&typeNames=bag:verblijfsobject&outputFormat=application/json&srsName=EPSG:4326 en een FES 2.0-filter in de parameter filter (PropertyIsEqualTo op postcode en op huisnummer, samen in fes:And). Postcode zonder spatie, hoofdletters. GeoJSON: coordinates = [lengte, breedte]. Kies het object zonder huisletter en toevoeging als er meer zijn. Gebruik NIET de PDOK Locatieserver (robots.txt verbiedt /bzk/locatieserver/*).
2. DUO CKAN-datastore: https://onderwijsdata.duo.nl/api/3/action/datastore_search?resource_id=…&filters={json}&fields=…&limit=100000
   - Vestigingen basisonderwijs met adres: dcc9c9a5-6d01-410b-967f-810557588ba4 (6.088 rijen; velden INSTELLINGSCODE, VESTIGINGSCODE, VESTIGINGSNAAM, STRAATNAAM, HUISNUMMER-TOEVOEGING, POSTCODE, PLAATSNAAM, GEMEENTENUMMER, DENOMINATIE, INTERNETADRES).
   - RIO onderwijslocaties met GPS: a7e3f323-6e46-4dca-a834-369d9d520aa8 (ONDERWIJSLOCATIECODE, BAG_ID, GPS_LATITUDE, GPS_LONGITUDE, POSTCODE, HUISNUMMER).
   - RIO vestiging naar locatie: c18ec7dd-aa4e-4b51-997c-782955f1aa38 (VESTIGINGSCODE, ONDERWIJSLOCATIECODE, BEGINDATUM, EINDDATUM).
   - Leerlingen per vestiging per schooljaar: 0e7c3205-761b-41cb-9149-30576619a944 (VESTIGINGSCODE, SCHOOLJAAR, AANTAL_LEERLINGEN; 1996 t/m 2025).
   - Schooladviezen per vestiging: 454c5dd4-6815-4b52-ae2f-b9eebc6afa6d (INSTELLINGSCODE, VESTIGINGSCODE, PEILJAAR, ADVIES, AANTAL_LEERLINGEN; nieuwste peiljaar 2024).
   - Prognose per vestiging tot 2045: f27c0d5c-59a0-41b9-b23f-f3a7ee03d4ba (INSTELLINGSCODE, VESTIGINGSCODE, JAAR, PROGNOSEAANTALLEN).

Wat je moet weten:
- Zonder limit geeft de datastore 1.000 rijen. Zet limit=100000 en controleer dat het aantal records gelijk is aan result.total. Het adviesbestand heeft 43.860 rijen voor één peiljaar: met limit=32000 mis je er stilletjes een kwart.
- filters is JSON in de URL (URL-encoden). Een waarde mag een lijst zijn: {"INSTELLINGSCODE": ["17RK", "17FL"]}. datastore_search_sql is niet toegestaan.
- Coördinaten: koppel vestiging aan RIO-locatie op postcode + het getal vooraan in HUISNUMMER-TOEVOEGING ("22-A" wordt 22). Lukt dat niet, gebruik de relatietabel met EINDDATUM leeg. Afstand met haversine.
- VESTIGINGSCODE heeft zes tekens in het adres- en leerlingenbestand ("17RK00"), maar twee in het advies- en prognosebestand ("00"): plak INSTELLINGSCODE ervoor.
- ADVIES is een getal: 1 vso, 2 pro, 3 vmbo-b, 4 vmbo-b/k, 5 vmbo-k, 6 vmbo-k/gt, 7 vmbo-gt, 8 vmbo-gt/havo, 9 havo, 10 havo/vwo, 11 vwo, 12 advies niet mogelijk (volgorde uit de DUO-toelichting; DUO zet de nummers er zelf niet bij, vermeld dat in de app).
- AANTAL_LEERLINGEN = -1 betekent 1 tot 4 leerlingen (AVG). Niet als getal optellen. Toon een onder- en bovengrens ("41% tot 81% havo of hoger") en grijs scholen uit waar de meeste cellen afgeschermd zijn.
- SCHOOLJAAR 2025 is schooljaar 2025/26, telling op 1 oktober. Fusies en verhuizingen geven sprongen: kijk naar andere vestigingen van dezelfde INSTELLINGSCODE en zeg het erbij in plaats van 'verdrievoudigd'.
- Gemeentenummers verschillen per bestand: "0344" (tekst) in het adressenbestand, 344 (getal) in de leerlingentellingen. Voor CBS wordt het GM0344.

Uitkomst: bovenaan het gevonden adres. Daaronder een kaart (Leaflet met de PDOK BRT-achtergrondkaart) met een stip per school, en een sorteerbare tabel: afstand, naam (link naar INTERNETADRES), denominatie, leerlingen 2015 en 2025 met een sparkline, prognose 2030, aandeel havo of hoger (onder- en bovengrens) en het meest gegeven advies. Onderaan bronvermelding: "DUO open onderwijsdata (CC BY), BAG via PDOK", met de peildata.

Probeer het zelf

De kern van het project in een klein script. Nodig: Python 3.9+ en requests (de Mac-Python met LibreSSL werkt hier: onderwijsdata.duo.nl en service.pdok.nl gaven geen TLS-fout). Getest op 2026-10-16; hieronder de echte uitvoer van die proefdraai.

# Basisscholen rond een adres: leerlingen door de tijd en schooladviezen.
# Bronnen: BAG (PDOK WFS) voor het adres, DUO open onderwijsdata (CKAN datastore) voor de scholen.
import math, re, collections, requests

UA = {"User-Agent": "mijn-schoolkeuzehulp/1.0 (+jij@voorbeeld.nl)"}
POSTCODE, HUISNUMMER, STRAAL_M = "3521AZ", 1, 1500
DUO = "https://onderwijsdata.duo.nl/api/3/action/datastore_search"
VESTIGINGEN = "dcc9c9a5-6d01-410b-967f-810557588ba4"   # alle vestigingen bo, met adres
LOCATIES = "a7e3f323-6e46-4dca-a834-369d9d520aa8"      # RIO onderwijslocaties, met GPS
RELATIES = "c18ec7dd-aa4e-4b51-997c-782955f1aa38"      # RIO vestiging -> onderwijslocatie
LEERLINGEN = "0e7c3205-761b-41cb-9149-30576619a944"    # historische leerlingentellingen per vestiging
ADVIES = "454c5dd4-6815-4b52-ae2f-b9eebc6afa6d"        # leerlingen per vestiging naar schooladvies
# Volgorde van de DUO-toelichting (ROD-codes 1 vso, 10 pro, 20 vmbo bl ... 80 geen advies mogelijk)
ADVIEZEN = {1: "vso", 2: "pro", 3: "vmbo-b", 4: "vmbo-b/k", 5: "vmbo-k", 6: "vmbo-k/gt", 7: "vmbo-gt",
            8: "vmbo-gt/havo", 9: "havo", 10: "havo/vwo", 11: "vwo", 12: "geen advies mogelijk"}

def duo(resource, **p):
    j = requests.get(DUO, params={"resource_id": resource, "limit": 100000, **p}, headers=UA, timeout=120).json()["result"]
    assert len(j["records"]) == j["total"], "niet alles opgehaald: verhoog limit of pagineer met offset"
    return j["records"]

# 1. Adres naar punt via de BAG-WFS (niet de Locatieserver: die is in robots.txt uitgesloten)
fes = ('<fes:Filter xmlns:fes="http://www.opengis.net/fes/2.0"><fes:And>'
       f'<fes:PropertyIsEqualTo><fes:ValueReference>postcode</fes:ValueReference><fes:Literal>{POSTCODE}</fes:Literal></fes:PropertyIsEqualTo>'
       f'<fes:PropertyIsEqualTo><fes:ValueReference>huisnummer</fes:ValueReference><fes:Literal>{HUISNUMMER}</fes:Literal></fes:PropertyIsEqualTo>'
       '</fes:And></fes:Filter>')
bag = requests.get("https://service.pdok.nl/lv/bag/wfs/v2_0", headers=UA, timeout=60, params={
    "service": "WFS", "version": "2.0.0", "request": "GetFeature", "typeNames": "bag:verblijfsobject",
    "outputFormat": "application/json", "srsName": "EPSG:4326", "filter": fes}).json()["features"]
vo = next((f for f in bag if not f["properties"]["huisletter"] and not f["properties"]["toevoeging"]), bag[0])
lon, lat = vo["geometry"]["coordinates"]
p = vo["properties"]
print(f"Adres: {p['openbare_ruimte']} {p['huisnummer']}, {p['postcode']} {p['woonplaats']} ({lat:.5f}, {lon:.5f})")

def afstand(lat1, lon1, lat2, lon2):   # haversine, in meters
    f1, f2 = math.radians(lat1), math.radians(lat2)
    a = math.sin((f2 - f1) / 2) ** 2 + math.cos(f1) * math.cos(f2) * math.sin(math.radians(lon2 - lon1) / 2) ** 2
    return 2 * 6371000 * math.asin(math.sqrt(a))

# 2. Alle vestigingen met een coördinaat: eerst op postcode + huisnummer, anders via de RIO-relatie
vest = duo(VESTIGINGEN, fields="INSTELLINGSCODE,VESTIGINGSCODE,VESTIGINGSNAAM,STRAATNAAM,HUISNUMMER-TOEVOEGING,POSTCODE,DENOMINATIE")
loc = duo(LOCATIES, fields="ONDERWIJSLOCATIECODE,POSTCODE,HUISNUMMER,GPS_LATITUDE,GPS_LONGITUDE")
op_adres = {(l["POSTCODE"], l["HUISNUMMER"]): l for l in loc}
op_code = {l["ONDERWIJSLOCATIECODE"]: l for l in loc}
via_rio = {r["VESTIGINGSCODE"]: op_code.get(r["ONDERWIJSLOCATIECODE"]) for r in duo(RELATIES) if r["EINDDATUM"] is None}
dichtbij = []
for s in vest:
    nr = re.match(r"\d+", s["HUISNUMMER-TOEVOEGING"] or "")
    l = op_adres.get((s["POSTCODE"], int(nr.group()) if nr else None)) or via_rio.get(s["VESTIGINGSCODE"])
    if l and l["GPS_LATITUDE"]:
        m = afstand(lat, lon, l["GPS_LATITUDE"], l["GPS_LONGITUDE"])
        if m <= STRAAL_M:
            dichtbij.append((m, s))
dichtbij.sort(key=lambda x: x[0])
print(f"{len(vest)} vestigingen in het basisonderwijs, {len(dichtbij)} binnen {STRAAL_M} m\n")

# 3. Leerlingen per schooljaar (SCHOOLJAAR = jaar waarin het schooljaar begint, telling 1 oktober)
brins = sorted({s["INSTELLINGSCODE"] for _, s in dichtbij})
lijst = str(brins).replace("'", '"')
ll = collections.defaultdict(dict)
for r in duo(LEERLINGEN, filters='{"INSTELLINGSCODE": %s}' % lijst):
    ll[r["VESTIGINGSCODE"]][r["SCHOOLJAAR"]] = r["AANTAL_LEERLINGEN"]

# 4. Schooladviezen in het nieuwste peiljaar. VESTIGINGSCODE is hier maar 2 tekens ('00'); -1 = 1 tot 4 leerlingen
adv = duo(ADVIES, filters='{"INSTELLINGSCODE": %s}' % lijst)
jaar = max(r["PEILJAAR"] for r in adv)
per = collections.defaultdict(lambda: collections.defaultdict(list))
for r in adv:
    if r["PEILJAAR"] == jaar:
        per[r["INSTELLINGSCODE"] + r["VESTIGINGSCODE"]][r["ADVIES"]].append(r["AANTAL_LEERLINGEN"])

print(f"{'afstand':>7}  {'school':32} {'2015':>4} {'2025':>4}  havo of hoger {jaar}")
for m, s in dichtbij:
    k = s["VESTIGINGSCODE"]
    reeks = ll[k]
    cellen = [(a, n) for a, ns in per.get(k, {}).items() for n in ns]
    hoog = sum(n for a, n in cellen if a >= 9 and n > 0)
    rest = sum(n for a, n in cellen if a < 9 and n > 0)
    h1 = sum(1 for a, n in cellen if a >= 9 and n == -1)   # afgeschermde cellen: 1 tot 4 leerlingen
    r1 = sum(1 for a, n in cellen if a < 9 and n == -1)
    if hoog + rest + h1 + r1 == 0:
        tekst = "geen adviezen"
    else:   # onder- en bovengrens, want -1 kan 1, 2, 3 of 4 zijn
        laag = (hoog + h1) / (hoog + h1 + rest + 4 * r1)
        hoogst = (hoog + 4 * h1) / (hoog + 4 * h1 + rest + r1)
        tekst = f"{laag:.0%}" if laag == hoogst else f"{laag:.0%} tot {hoogst:.0%}"
        meest = max(cellen, key=lambda c: c[1])
        tekst = f"{tekst:12} (meest: {ADVIEZEN[meest[0]]})" if meest[1] > 0 else tekst
    print(f"{m:6.0f}m  {s['VESTIGINGSNAAM'][:32]:32} {reeks.get(2015, '-'):>4} {reeks.get(2025, '-'):>4}  {tekst}")
Uitvoer
Adres: Stadsplateau 1, 3521AZ Utrecht (52.08960, 5.10775)
6088 vestigingen in het basisonderwijs, 13 binnen 1500 m

afstand  school                           2015 2025  havo of hoger 2024
   616m  Kindercampus Molenpark            182  152  59% tot 73%  (meest: vwo)
   701m  KBS De Pijlstaart                 146  191  41% tot 81%  (meest: havo/vwo)
   823m  KBS De Catharijnepoort            324  157  52% tot 81%  (meest: havo/vwo)
  1124m  Openbare Basisschool De Kleine D  186  341  17% tot 57%  (meest: vmbo-gt/havo)
  1133m  Christelijke Basisschool De Brug  318  285  65% tot 72%  (meest: vwo)
  1207m  ODBS Pieterskerkhof               190  219  100%         (meest: havo/vwo)
  1215m  Openbare Basisschool De Koekoek   340  336  76% tot 93%  (meest: vwo)
  1283m  Kathedrale Koorschool              75   53  43% tot 82%  (meest: vwo)
  1295m  Vrije School Utrecht              325  437  78%          (meest: vwo)
  1346m  Asch van Wijckschool              224  201  49% tot 69%  (meest: vwo)
  1411m  Basisschool Agatha Snellen        220  211  75% tot 92%  (meest: vwo)
  1465m  Basisschool Anne Frank            191  186  20% tot 80%
  1472m  Johan de Witt                       -  207  55%          (meest: havo/vwo)

Waar het misgaat

  • De datastore geeft zonder limit 1.000 rijen en kapt stil af. Met limit=32000 kreeg het adviesbestand voor 2024 maar 32.000 van de 43.860 rijen, en leek het of 282 kinderen een vso-advies kregen in plaats van 430. Zet limit=100000 en controleer len(records) == total.
  • -1 is geen getal maar '1 tot 4 leerlingen'. In peiljaar 2024 is 69% van de adviescellen afgeschermd (30.235 van 43.860). Tel je alleen de bekende cellen, dan overschat je grote categorieën (vwo kwam zo op 25%) en verdwijnen kleine (pro bij gewone basisscholen). Toon een bandbreedte.
  • ADVIES 1 tot 12 heeft in de API geen labels. De volgorde vso, pro, vmbo-b … vwo, advies niet mogelijk komt uit de DUO-toelichting (ROD-codes 1, 10, 20 … 80) en klopt met het speciaal basisonderwijs, maar DUO bevestigt de nummering niet letterlijk. Zeg dat erbij en controleer bij een nieuwe versie van het bestand.
  • Twee soorten vestigingscode: "17RK00" in het adres- en leerlingenbestand, "00" in het advies- en prognosebestand. Een join op VESTIGINGSCODE alleen koppelt alle hoofdvestigingen van Nederland aan elkaar.
  • Fusies, verhuizingen en sluitingen geven sprongen in de reeks (De Kleine Dichter: 125 naar 341 leerlingen op 17RK00 omdat 17RK01 erin opging). Optellen per instelling is ook niet altijd goed (Pieterskerkhof en KC Cartesius). Laat de reeks per vestiging zien en meld het als een andere vestiging van dezelfde instelling ophield.
  • Eén adres kan meerdere verblijfsobjecten hebben (Stadsplateau 1 en 1T). Neem niet blind het eerste resultaat. En gebruik de BAG-WFS of de OGC API, niet de Locatieserver: die staat in robots.txt van api.pdok.nl als verboden pad.
  • Een school dichtbij is niet per se een school waar je kind terechtkan. Veel gemeenten hebben een centraal aanmeldbeleid of voorrangsregels, en hemelsbrede afstand is geen looproute. Toon de afstand als indicatie en link naar de site van de school.

Verder bouwen

  • Toetsresultaten erbij: het bestand 'Referentieniveau per toets en vestiging' (89920163-8b28-4e1a-b322-c3bb90f74f41) geeft per school hoeveel leerlingen 1F, 2F en 1S haalden voor taal en rekenen. Gebruik dat en niet de gemiddelde eindscore: in peiljaar 2024 gebruikten scholen zes verschillende doorstroomtoetsen (LIB 3.015 rijen, IEP 2.676, ROUTE 8 562, DIA 360, AMN 112, DOE 39) met elk een eigen schaal.
  • Waar komen de kinderen vandaan? 'Leerlingen po per vestiging naar postcode4' (402c5a69-eea8-41b4-b8ad-ecac301c28a8) laat per school zien uit welke postcodegebieden de leerlingen komen. Zo zie je of een school echt een buurtschool is en hoe ver ouders reizen.
  • Schoolgebouw uit de BAG: met het BAG_ID uit RIO haal je bouwjaar en oppervlakte op en reken je m² per leerling uit. Of kleur de kaart naar de schoolweging ('Schoolgewicht per schoolvestiging', 59383f9c-2b0b-4aae-97f9-dab8d92cd873), zodat je adviezen eerlijker kunt vergelijken.
  • Hetzelfde voor de middelbare school: het adressenbestand voor het voortgezet onderwijs (5187f8d5-ff9c-4284-8e06-4311f0354956) en de vo-bestanden in dezelfde catalogus. Toon dan bij elke basisschool naar welke niveaus de kinderen gingen en welke vo-scholen in de buurt die niveaus aanbieden.

De bronnen2

DUO open onderwijsdata

Dienst Uitvoering Onderwijs (ministerie van OCW)Nederland, schoolvestiging

Alle scholen en opleidingen met adres, en per schoolvestiging het aantal leerlingen, schooladviezen, doorstroomtoetsscores, examencijfers en prognoses. Van basisschool tot universiteit. DUO publiceert het als downloads en als API (56 datasets met 274 tabellen in de datastore op 16 oktober 2026).

OpenAPIDownloadOnderwijsBevolking
Actueel
bijgewerkt 10 uur geleden

Basisregistratie Adressen en Gebouwen (BAG)

Kadaster, via PDOKNederland, adres en gebouw

Alle adressen en gebouwen van Nederland met bouwjaar, oppervlakte en gebruiksdoel. De ruggengraat voor alles wat op adresniveau gekoppeld moet worden.

OpenKaartlaagAPIDownloadWonen en ruimte
Bereikbaar
dagelijks

Meer om te bouwen