Veel bedrijven met vestigingen houden een tabel bij: vestiging Gouda doet postcode 2741 tot en met 2900, Rotterdam 2901 tot en met 3399. Zo’n tabel ziet er in Excel prima uit. Op de kaart zie je pas wat hij echt doet. Klik op een vestiging.
De vestigingen zijn verzonnen. De postcodegebieden, de adressen en de afstanden zijn echt. Klik op een vestiging in de lijst, en de kaart zoomt in op haar gebied. “Niet ingedeeld” toont alle postcodes die bij geen enkele vestiging horen, met een lijst die je als CSV downloadt. Zoom met het muiswiel of de knoppen, sleep om te schuiven, en “Heel Nederland” brengt je terug naar het overzicht. Zet de postcodenummers aan om te zien welk gebied welk nummer heeft.
De tabel die de klant bijhoudt
Dit is de tabel achter de kaart. Er zitten twee fouten in, zoals in echte tabellen. Vind je ze al?
CREATE TABLE dbo.Werkgebied ( Vestiging nvarchar(40) NOT NULL, PostcodeVan char(4) NOT NULL, PostcodeTot char(4) NOT NULL);
INSERT INTO dbo.Werkgebied (Vestiging, PostcodeVan, PostcodeTot) VALUES (N'Amsterdam', '1000', '1199'), (N'Almere', '1200', '1399'), (N'Alkmaar', '1400', '1999'), (N'Haarlem', '2000', '2199'), (N'Den Haag', '2200', '2740'), (N'Gouda', '2741', '2900'), (N'Rotterdam', '2901', '3399'), (N'Utrecht', '3400', '3999'), (N'Breda', '3990', '4999'), (N'Eindhoven', '5000', '5999'), (N'Maastricht', '6000', '6499'), (N'Arnhem', '6500', '7299'), (N'Enschede', '7400', '7999'), (N'Zwolle', '8000', '8999'), (N'Groningen', '9000', '9999');De postcodes staan als char(4), niet als getal. Een postcode heeft altijd vier cijfers, dus '2741' < '2900' klopt als tekst ook. Zo vergelijk je tekst met tekst, en kan SQL Server een index op de postcode gebruiken.
Koppelen met BETWEEN
De postcodegebieden komen uit het bestand Postcodegebieden van postcode.store: alle 4.071 gebieden van vier cijfers, met hun vorm van het CBS. De koppeling is één BETWEEN:
SELECT g.Postcode, w.VestigingFROM dbo.Postcodegebieden AS gLEFT JOIN dbo.Werkgebied AS w ON g.Postcode BETWEEN w.PostcodeVan AND w.PostcodeTot;Dat geeft 4.079 rijen, terwijl er 4.071 gebieden zijn. Acht gebieden komen dus twee keer terug. En 42 rijen hebben geen vestiging. Dat zijn de twee fouten.
Controle 1: postcodes zonder vestiging
SELECT g.PostcodeFROM dbo.Postcodegebieden AS gWHERE NOT EXISTS ( SELECT 1 FROM dbo.Werkgebied AS w WHERE g.Postcode BETWEEN w.PostcodeVan AND w.PostcodeTot);Het resultaat: 42 gebieden, van 7311 tot en met 7399. Dat is Apeldoorn met Ugchelen, samen 97.909 adressen. Arnhem stopt bij 7299 en Enschede begint bij 7400. Niemand heeft 7300 tot en met 7399. Een klant uit Apeldoorn valt in deze tabel bij geen enkele vestiging.
Op de kaart staat dit gebied rood gearceerd. Kies “Niet ingedeeld”, en je krijgt de lijst van alle 42 postcodes met hun plaats en hun aantal adressen.
Controle 2: reeksen die overlappen
Twee reeksen overlappen als de ene begint voordat de andere ophoudt, en andersom. Koppel de tabel aan zichzelf:
SELECT a.Vestiging, b.Vestiging, CASE WHEN a.PostcodeVan > b.PostcodeVan THEN a.PostcodeVan ELSE b.PostcodeVan END AS Van, CASE WHEN a.PostcodeTot < b.PostcodeTot THEN a.PostcodeTot ELSE b.PostcodeTot END AS TotFROM dbo.Werkgebied AS aJOIN dbo.Werkgebied AS b ON a.Vestiging < b.Vestiging AND a.PostcodeVan <= b.PostcodeTot AND b.PostcodeVan <= a.PostcodeTot;a.Vestiging < b.Vestiging zorgt dat je elk paar één keer krijgt, en niet een vestiging met zichzelf. Het resultaat is één rij: Breda en Utrecht delen 3990 tot en met 3999. Dat zijn acht gebieden in Houten en omgeving, met 23.888 adressen. Wie daar belt, krijgt twee vestigingen.
Controle 3: dichter bij een andere vestiging
Een reeks volgt de nummers, niet de kaart. Postcode 3888 in Uddel valt in de reeks van Utrecht, maar ligt op de Veluwe, 49,2 km van Utrecht. Postcode 6029 in Sterksel ligt in Brabant en valt toch bij Maastricht, op 56,3 km.
Om dat te tellen heb je per gebied een middelpunt nodig, en per vestiging een plek. Het adressenbestand Zilver heeft voor elk adres RD-coördinaten in meters. Het gemiddelde van alle adressen in een postcode is het middelpunt. De afstand is dan gewoon Pythagoras:
WITH midden AS ( SELECT PostcodeNummers AS Postcode, COUNT(*) AS Adressen, AVG(TRY_CAST(XCoordinaat AS float)) AS X, AVG(TRY_CAST(YCoordinaat AS float)) AS Y FROM dbo.PostcodetabelZilver GROUP BY PostcodeNummers)SELECT g.Postcode, w.Vestiging AS VolgensTabel, dichtst.Vestiging AS Dichtstbij, m.AdressenFROM dbo.Postcodegebieden AS gJOIN midden AS m ON m.Postcode = g.PostcodeJOIN dbo.Werkgebied AS w ON g.Postcode BETWEEN w.PostcodeVan AND w.PostcodeTotCROSS APPLY ( SELECT TOP (1) v.Vestiging FROM dbo.Vestiging AS v -- per vestiging een X en Y in RD ORDER BY SQUARE(m.X - v.X) + SQUARE(m.Y - v.Y)) AS dichtstWHERE dichtst.Vestiging <> w.Vestiging AND NOT EXISTS ( -- de overlap uit controle 2 laten we hier weg SELECT 1 FROM dbo.Werkgebied AS w2 WHERE g.Postcode BETWEEN w2.PostcodeVan AND w2.PostcodeTot AND w2.Vestiging <> w.Vestiging);Voor de volgorde is de wortel niet nodig: wie het kleinste kwadraat heeft, heeft ook de kortste afstand. Het resultaat is groot. 1.077 gebieden met samen 2.104.292 adressen liggen dichter bij een andere vestiging dan bij die van hun reeks. Bij Utrecht alleen al zijn het 249.287 adressen, en 125.358 daarvan liggen dichter bij Almere.
Dat hoeft geen fout te zijn. Een vestiging kan bewust verder rijden, bijvoorbeeld omdat het water ertussen ligt. Maar je wilt het weten. Zet op de kaart “Dichter bij een andere vestiging” aan, en de gebieden krijgen strepen in de kleur van de vestiging die het dichtstbij is.
De vorm op de kaart
Het bestand Postcodegebieden heeft per gebied een kolom Vorm: de grens als tekst in WKT, in lengte- en breedtegraden. SQL Server leest die met geography::STGeomFromText. Daar zit één val in. Bij een deel van de vormen loopt de ring de andere kant op. SQL Server ziet dan alles buiten het gebied als het gebied. Postcode 1011 in Amsterdam kwam zo uit op 510 miljoen km²: de hele aarde.
De oplossing is twee stappen:
WITH v AS ( SELECT Postcode, geography::STGeomFromText(Vorm, 4326).MakeValid() AS g0 FROM dbo.Postcodegebieden)SELECT Postcode, CASE WHEN g0.EnvelopeAngle() > 90 THEN g0.ReorientObject() ELSE g0 END AS VormFROM v;EnvelopeAngle is groter dan 90 graden als een vorm meer dan een halve wereldbol beslaat. Geen postcodegebied is zo groot, dus dan draai je hem om. Daarna komen alle 4.071 gebieden samen uit op 35.074 km², en postcode 1011 op 1,06 km².
Voor de kaart op deze pagina is de vorm vereenvoudigd tot 8% van de punten, met mapshaper. Mapshaper vereenvoudigt een gedeelde grens één keer, dus buren blijven op elkaar aansluiten. Het bestand voor alle gebieden is daarna 758 KB.
Klopt de som?
Het script dat de data voor de kaart maakt, telt alle adressen op de kaart op en vergelijkt dat met het adressenbestand. Op de kaart staan 9.590.124 adressen. In het adressenbestand staan er 663 meer, in postcodes die het CBS nog niet kent, zoals 1116 met 389 adressen. Ook die controle is één keer bewust kapotgemaakt, en toen stopte het script met een fout.
Zelf proberen
Je hebt twee bestanden nodig: Postcodegebieden voor de vormen, en een adressenbestand met coördinaten voor de afstanden. Je eigen tabel met reeksen zet je ernaast. Draai eerst controle 1 en 2: die vinden in een paar seconden de fouten die anders pas opvallen als een klant belt.
In Power BI kleur je de gebieden met de kolom Vorm en je eigen vestiging als legenda. Wil je losse adressen op de kaart in plaats van gebieden, lees dan postcodes op een kaart. Hoe Nederland per bouwjaar is gegroeid, zie je op de bouwjaarkaart.
Veelgestelde vragen
Hoe koppel ik postcodereeksen aan vestigingen in SQL Server?
Leg per vestiging een reeks vast met een kolom PostcodeVan en PostcodeTot. Koppel de postcodegebieden met BETWEEN op die twee kolommen. Bewaar de postcodes als char(4), dan vergelijkt SQL Server ze correct als tekst.
Hoe vind ik postcodes die bij geen enkele vestiging horen?
Zoek met NOT EXISTS alle postcodegebieden waarvoor geen reeks bestaat waar de postcode tussen PostcodeVan en PostcodeTot valt. In het voorbeeld waren dat 42 gebieden rond Apeldoorn, met 97.909 adressen.
Hoe vind ik overlappende postcodereeksen?
Koppel de tabel aan zichzelf. Twee reeksen overlappen als de begin-postcode van de ene niet groter is dan de eind-postcode van de andere, en andersom. Met a.Vestiging < b.Vestiging krijg je elk paar maar één keer.
Waarom geeft geography::STGeomFromText een gebied zo groot als de aarde?
De ring van de vorm loopt dan de verkeerde kant op, en SQL Server leest alles buiten het gebied als het gebied. Gebruik MakeValid en daarna ReorientObject als EnvelopeAngle groter is dan 90. Postcode 1011 kwam zo uit op 1,06 km².
Is een postcodereeks hetzelfde als een regio?
Nee. Postcodenummers volgen de regio ongeveer, maar niet precies. In het voorbeeld lag 1.077 van de gebieden dichter bij een andere vestiging dan bij die van hun reeks, met samen 2.104.292 adressen.
Reacties