Elke organisatie die al een paar jaar adresgegevens verzamelt, heeft hetzelfde probleem: een grote, rommelige adrestabel met onbekende kwaliteit. Adressen ingevoerd via webformulieren door gebruikers die niet opletten. Records gemigreerd vanuit een verouderd CRM dat invoer niet valideerde. Bulkimports vanuit de Excel van een partner in een incompatibel formaat. Bedrijfslocaties die zijn verplaatst nadat de data werd verzameld.
Logistiek draaien op slechte adresdata veroorzaakt mislukte afleveringen. Marketing erop draaien verspilt direct mail-budgetten. Vastgoedwaarderingen erop uitvoeren produceert onjuiste verzorgingsgebiedanalyses. Voordat een van die operaties plaatsvindt, moeten de adressen worden opgeschoond en gevalideerd.
Geocodering is de meest effectieve manier om dit op schaal te doen. Stuur een onbewerkte adresstring naar de Geocoding API; ontvang gestructureerde onderdelen, geografische coördinaten en een betrouwbaarheidsscore terug. Adressen die met hoge betrouwbaarheid geocoderen zijn vrijwel zeker geldig. Adressen die een lage betrouwbaarheid of helemaal geen overeenkomst retourneren, hebben menselijke controle nodig.
Deze tutorial bouwt een volledig Python-script dat een CSV met adressen leest, elk adres geocodeert via de MapAtlas Geocoding API, de resultaten met betrouwbaarheidsscores schrijft en records met lage betrouwbaarheid markeert voor handmatige controle. Het handelt snelheidsbeperkingen, voorbijgaande netwerkfouten en EU-adresformaatbijzonderheden af die tutorials gericht op de Amerikaanse markt missen.
Waarom adresdata verslechtert
Adresskwaliteit verslechtert om voorspelbare redenen:
Handmatige invoerfouten. Gebruikers van webformulieren typen snel, autocorrectie verminkt straatnamen en validatie die elke niet-lege string accepteert laat rommel door. Een studie van B2C-afrekeningen vond dat 7 tot 12% van handmatig ingevoerde adressen fouten bevat die ernstig genoeg zijn om afleveringen te laten mislukken.
Bedrijfsverplaatsingen. Een B2B-database verzameld twee jaar geleden heeft ongeveer 10 tot 15% van de adressen die niet meer overeenkomen met de huidige bedrijfslocatie vanwege verhuizingen, fusies en sluitingen.
Formaatinconsistenties. Data verzameld uit meerdere bronnen gebruikt verschillende conventies: volledige landnamen versus ISO-codes, "St." versus "Straat", huisnummer voor of na de straatnaam, "flat" versus "apt" versus "wohnung". Een geocoder normaliseert dit alles naar gestructureerde uitvoer.
Migraties van verouderde systemen. Adresvelden die over meerdere databasekolommen waren verspreid, worden bij migratie vaak samengevoegd, waarbij structuur verloren gaat. Vrije-tekst adresvelden uit oudere systemen kunnen notities, referenties of opmaak bevatten die geen deel uitmaken van het werkelijke adres.
De geocoderingsbenadering handelt dit alles af omdat het vertrouwt op geografische overeenkomsten, niet op stringovereenkomsten. Een onjuist opgemaakt adres kan toch correct worden geocodeerd als de onderliggende locatiedata overeenkomt.
Betrouwbaarheidsscores begrijpen
De MapAtlas Geocoding API retourneert een confidence-eigenschap bij elk resultaat, variërend van 0,0 tot 1,0. Dit geeft aan hoe nauw het geretourneerde resultaat overeenkomt met de invoerzoekopdracht, rekening houdend met formaatafwijkingen, afkortingen en dubbelzinnigheid.
| Betrouwbaarheid | Interpretatie | Aanbevolen actie |
|---|---|---|
| 0,90 tot 1,00 | Exacte of bijna exacte overeenkomst | Automatisch accepteren |
| 0,85 tot 0,89 | Sterke overeenkomst, kleine formaatafwijkingen | Accepteren met logging |
| 0,60 tot 0,84 | Gedeeltelijke overeenkomst, straat gevonden maar huisnummer onzeker | Markeren voor handmatige controle |
| 0,40 tot 0,59 | Dubbelzinnig, lokaalheid komt overeen maar niet specifiek adres | Weigeren of escaleren |
| 0,00 tot 0,39 | Geen betekenisvolle overeenkomst | Weigeren, waarschijnlijk ongeldig |
Dit zijn startpunten. Voor een logistieke operatie waarbij mislukte afleveringen duur zijn, verstreng de drempel voor automatisch accepteren naar 0,92 of hoger. Voor een marketingmailinglijst waarbij de kosten van een handmatige controle opwegen tegen een paar slechte adressen, verruim het naar 0,80.
De API retourneert ook een match_type-eigenschap die aangeeft op welk niveau van de adreshiërarchie overeenkomst is gevonden: point (exact gebouw), interpolated (positie geschat tussen bekende huisnummers), street (straat gevonden maar huisnummer niet), of locality (alleen de stad komt overeen).
Het Python-validatiescript
Installeer afhankelijkheden:
pip install requests pandas tqdm
Het script leest een CSV met een kolom address (of afzonderlijke kolommen street, city, country), geocodeert elke rij en schrijft een nieuwe CSV met toegevoegde validatieresultaten.
#!/usr/bin/env python3
"""
bulk_geocode.py, Validate a CSV of addresses using the MapAtlas Geocoding API.
Input CSV must have either:
- An 'address' column (full address string), or
- 'street', 'city', and 'country' columns (will be concatenated)
Outputs a new CSV with added columns:
geocoded_label, geocoded_lat, geocoded_lng, confidence, match_type, status
"""
import csv
import time
import logging
import requests
import pandas as pd
from tqdm import tqdm
from pathlib import Path
# ── Configuration ──────────────────────────────────────────────────────────────
API_KEY = 'YOUR_API_KEY'
API_BASE = 'https://api.mapatlas.eu/geocoding/v1/search'
INPUT_CSV = 'addresses.csv'
OUTPUT_CSV = 'addresses_validated.csv'
RATE_LIMIT_RPS = 5 # Requests per second (stay within your plan limits)
RETRY_ATTEMPTS = 3 # Retries on transient errors
RETRY_DELAY_S = 2.0 # Seconds between retries
# Confidence thresholds
ACCEPT_THRESHOLD = 0.85
REVIEW_THRESHOLD = 0.60
logging.basicConfig(level=logging.INFO, format='%(levelname)s %(message)s')
log = logging.getLogger(__name__)
# ── Geocoding function ─────────────────────────────────────────────────────────
def geocode_address(address_str: str) -> dict:
"""
Geocode a single address string. Returns a dict with result fields.
Retries on network errors and 429 rate-limit responses.
"""
params = {
'text': address_str,
'key': API_KEY,
'size': 1,
}
for attempt in range(RETRY_ATTEMPTS):
try:
resp = requests.get(API_BASE, params=params, timeout=10)
if resp.status_code == 429:
# Rate limited, wait and retry
wait = float(resp.headers.get('Retry-After', RETRY_DELAY_S * (attempt + 1)))
log.warning(f'Rate limited. Waiting {wait:.1f}s before retry {attempt + 1}.')
time.sleep(wait)
continue
resp.raise_for_status()
data = resp.json()
if not data.get('features'):
return {'geocoded_label': '', 'geocoded_lat': None, 'geocoded_lng': None,
'confidence': 0.0, 'match_type': 'no_match', 'status': 'reject'}
feature = data['features'][0]
props = feature['properties']
coords = feature['geometry']['coordinates'] # [lng, lat]
confidence = round(float(props.get('confidence', 0.0)), 4)
match_type = props.get('match_type', 'unknown')
if confidence >= ACCEPT_THRESHOLD:
status = 'accept'
elif confidence >= REVIEW_THRESHOLD:
status = 'review'
else:
status = 'reject'
return {
'geocoded_label': props.get('label', ''),
'geocoded_lat': round(coords[1], 6),
'geocoded_lng': round(coords[0], 6),
'confidence': confidence,
'match_type': match_type,
'status': status,
}
except requests.exceptions.RequestException as exc:
log.warning(f'Network error on attempt {attempt + 1}: {exc}')
if attempt < RETRY_ATTEMPTS - 1:
time.sleep(RETRY_DELAY_S * (attempt + 1))
# All retries exhausted
return {'geocoded_label': '', 'geocoded_lat': None, 'geocoded_lng': None,
'confidence': 0.0, 'match_type': 'error', 'status': 'error'}
# ── Main processing loop ───────────────────────────────────────────────────────
def main():
df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')
# Build address string from available columns
if 'address' in df.columns:
df['_query'] = df['address']
elif all(c in df.columns for c in ['street', 'city', 'country']):
df['_query'] = df['street'] + ', ' + df['city'] + ', ' + df['country']
else:
raise ValueError("CSV must have 'address' or 'street'+'city'+'country' columns.")
results = []
sleep_interval = 1.0 / RATE_LIMIT_RPS
for query in tqdm(df['_query'], desc='Geocoding', unit='addr'):
result = geocode_address(query.strip())
results.append(result)
time.sleep(sleep_interval)
results_df = pd.DataFrame(results)
output_df = pd.concat([df.drop(columns=['_query']), results_df], axis=1)
output_df.to_csv(OUTPUT_CSV, index=False, quoting=csv.QUOTE_NONNUMERIC)
# Summary
total = len(output_df)
accept = (output_df['status'] == 'accept').sum()
review = (output_df['status'] == 'review').sum()
reject = (output_df['status'] == 'reject').sum()
errors = (output_df['status'] == 'error').sum()
log.info(f'\n── Results ────────────────────────────────')
log.info(f'Total processed : {total:,}')
log.info(f'Accept (≥{ACCEPT_THRESHOLD}) : {accept:,} ({accept/total:.1%})')
log.info(f'Review : {review:,} ({review/total:.1%})')
log.info(f'Reject : {reject:,} ({reject/total:.1%})')
log.info(f'Errors : {errors:,} ({errors/total:.1%})')
log.info(f'Output written to {OUTPUT_CSV}')
if __name__ == '__main__':
main()
Voer het uit:
python bulk_geocode.py
Voor een CSV met 10.000 adressen bij 5 verzoeken per seconde is het script in ongeveer 35 minuten klaar. De voortgangsbalk (via tqdm) toont real-time doorvoer en geschatte voltooiingstijd.
De uitvoer verwerken
De uitvoer-CSV voegt zes kolommen toe aan uw invoergegevens:
address, ..., geocoded_label, geocoded_lat, geocoded_lng, confidence, match_type, status
Filter op status om drie uitvoerbestanden te genereren:
# After running main(), split into acceptance tiers:
df = pd.read_csv('addresses_validated.csv')
df[df['status'] == 'accept'].to_csv('addresses_clean.csv', index=False)
df[df['status'] == 'review'].to_csv('addresses_review.csv', index=False)
df[df['status'] == 'reject'].to_csv('addresses_reject.csv', index=False)
print(f"Clean: {len(df[df['status']=='accept']):,} addresses ready for use")
print(f"Review: {len(df[df['status']=='review']):,} addresses for manual check")
print(f"Reject: {len(df[df['status']=='reject']):,} addresses to discard or fix")
Het bestand addresses_review.csv vereist menselijke aandacht. Typische controlepatronen:
- Match type
street(betrouwbaarheid 0,65 tot 0,80): De straat werd gevonden maar het huisnummer niet. Waarschijnlijk een nieuw gebouw, een landelijk adres met beperkte dekking, of een typfout in het huisnummer. Controleer de oorspronkelijke bron. - Match type
locality(betrouwbaarheid 0,50 tot 0,65): Alleen de stad komt overeen. De straatnaam is waarschijnlijk fout gespeld of bestaat niet in de data. Zoek het adres op in een postadressengids. - Lage betrouwbaarheid bij duidelijk geldige adressen: Controleer op een mismatch tussen land en taal. Een Nederlands adres zonder landbeperking kan worden gecodeeerd naar een gelijknamige Duitse of Belgische stad.
EU-adresformaatbijzonderheden om te kennen
Bulk geocodeeringgidsen geschreven voor de Amerikaanse markt slaan de formaatafwijkingen over die EU-datasets problemen geven. Dit zijn de meest voorkomende problemen:
Duitsland, huisnummervolgorde. Duitse adressen gebruiken het formaat {straat} {nummer}: Berliner Straße 42. Veel CRM's slaan adressen op in het formaat {nummer} {straat} omdat ze zijn gebouwd voor de Britse of Amerikaanse conventie. Als uw Duitse adressen met lage betrouwbaarheid geocoderen, probeer dan het nummer en de straatnaam in de zoekopdracht om te draaien voordat u ze indient.
Frankrijk, arrondissementen. Parijse adressen bevatten een arrondissement (1e tot 20e) als onderdeel van de postcode: 75001 tot 75020. Zoekopdrachten die het arrondissement weglaten en gewoon Paris gebruiken, worden geocodeerd naar het stadscentrum, niet naar het specifieke district. Dit verschijnt als een locality-overeenkomst met lage betrouwbaarheid in plaats van een address-overeenkomst.
Nederland, postcodeformaat. Nederlandse postcodes volgen een strikt DDDD LL-patroon (4 cijfers, spatie, 2 hoofdletters). Codes opgeslagen zonder spatie (1012LG in plaats van 1012 LG) of met kleine letters worden correct geocodeerd, maar als u het postcodeformaat afzonderlijk valideert, normaliseer naar hoofdletters met een spatie.
België, taalambiguïteit. Sommige Belgische gemeenten hebben verschillende Franse en Nederlandse namen (Luik/Liège, Gent/Gand). De API verwerkt beide, maar inconsistente naamgeving in uw dataset (sommige records in het Frans, sommige in het Nederlands) kan verschillende betrouwbaarheidsniveaus produceren. Normaliseer naar één taal per regio voordat u geocodeert.
Spanje en Italië, variaties in straatprefixen. "Calle", "Carrer", "Via", "Viale" zijn allemaal geldige straattype-prefixen en kunnen afgekort verschijnen ("C/") in records. De geocoder verwerkt veelgebruikte afkortingen, maar ongebruikelijke afkortingen uit verouderde systemen kunnen een normalisatiestap vereisen.
Doorvoer verhogen met gelijktijdige verzoeken
Het sequentiële script is veilig en eenvoudig maar traag. Voor grotere datasets vervangt u de sequentiële lus door een gelijktijdige uitvoerder:
from concurrent.futures import ThreadPoolExecutor, as_completed
def main_concurrent(max_workers=10):
df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')
# ... (same setup as before) ...
results = [None] * len(df)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_idx = {
executor.submit(geocode_address, row['_query'].strip()): idx
for idx, row in df.iterrows()
}
for future in tqdm(as_completed(future_to_idx), total=len(df), desc='Geocoding'):
idx = future_to_idx[future]
results[idx] = future.result()
# ... (same output writing as before) ...
Met 10 gelijktijdige workers bij 5 verzoeken per seconde per worker bereikt de doorvoer ongeveer 50 verzoeken per seconde, 10.000 adressen in minder dan 4 minuten. Controleer de limiet voor gelijktijdige verzoeken van uw MapAtlas-abonnement voordat u max_workers verhoogt.
Voor organisaties die adresvalidatie als terugkerende operatie uitvoeren, nieuwe CRM-imports wekelijks opschonen of afleveradressen 's nachts valideren, zie de pagina Logistiek en Bezorging voor informatie over hoe MapAtlas-integraties passen in operationele workflows.
Als u adresautoaanvulling bouwt om slechte data van tevoren te voorkomen (fouten bij de bron opvangen in plaats van ze in bulk op te schonen), behandelt Address Autocomplete API: How One Field Lifts Checkout Conversion by 35% de frontend-implementatie.
Werken met de coördinatenuitvoer
De gevalideerde uitvoer bevat geocoded_lat en geocoded_lng voor elk geaccepteerd adres. Deze coördinaten openen analysemogelijkheden die niet mogelijk waren met onbewerkte adresstrings:
- Afstandsberekeningen. Berekenen van rechte-lijnafstanden tussen een magazijn en elk afleveradres om verzendkostentiers te schatten.
- Verzorgingsgebiedanalyse. Gevalideerde klantlocaties op een kaart uitzetten om te zien waar de vraag geografisch clustert.
- Bezorgzoneassignatie. Elk adres toewijzen aan een bezorgzone door te controleren of de coördinaten binnen een zonepolygoon vallen.
- Duplicaatdetectie. Twee records met dezelfde coördinaten (binnen een paar meter) zijn waarschijnlijk duplicaten, zelfs als de adresstrings in opmaak verschillen.
Voor NAP-consistentie (naam, adres, telefoon) en de impact ervan op AI-zoekzichtbaarheid, in het bijzonder relevant voor lokale bedrijfsadresbases, legt NAP-consistentie voor AI-zoekopdrachten: waarom niet-overeenkomende adressen uw ChatGPT-zichtbaarheid vernielen uit waarom geocodeervalideerde adressen de juiste basis zijn voor gestructureerde data-opmaak.
Samenvatting
Bulk geocoderen met de MapAtlas Geocoding API geeft u:
- Gevalideerde adressen met betrouwbaarheidsscores zodat u weet welke records u automatisch kunt vertrouwen en welke controle nodig hebben.
- Gestructureerde onderdelen (straat, huisnummer, postcode, stad, land) genormaliseerd vanuit elk invoerformaat.
- Geografische coördinaten voor elk geaccepteerd adres, die ruimtelijke analyses en routering mogelijk maken.
- Ondersteuning voor EU-adresformaten ingebouwd in de API, niet iets wat u in voorverwerking hoeft af te handelen.
Het Python-script draait bij 5 adressen per seconde sequentieel, tot 50 per seconde met gelijktijdige workers. Budget voor 10.000 adressen 4 tot 40 minuten afhankelijk van gelijktijdigheid. Uitvoer is een nette CSV met drie niveaus: accepteren, controleren, weigeren.
Meld u aan voor een gratis MapAtlas API-sleutel om te beginnen. De Geocoding API ondersteunt bulkzoekopdrachten op alle abonnementen, zie prijzen voor tarieven per verzoek en maandelijkse gratis limieten.
Veelgestelde vragen
Hoe lang duurt het om 10.000 adressen te geocoderen met de MapAtlas API?
Bij een conservatieve snelheid van 5 verzoeken per seconde met een kleine slaapbuffer duurt het geocoderen van 10.000 adressen ongeveer 35 tot 40 minuten. Met gelijktijdige verzoeken (10 workers) en geschikte snelheidslimieten daalt dit tot minder dan 5 minuten. Het Python-script in deze tutorial bevat zowel sequentiële als gelijktijdige opties.
Welke betrouwbaarheidsdrempel moet ik gebruiken voor adresvalidatie?
Een praktisch drieniveausysteem werkt goed: accepteer adressen met een betrouwbaarheid boven 0,85, markeer voor handmatige controle tussen 0,60 en 0,85, en wijs af onder 0,60. De exacte drempelwaarden hangen af van hoe kritisch adresnauwkeurigheid is voor uw gebruik, logistieke operaties moeten strengere drempelwaarden hanteren dan marketingcampagnes.
Verwerkt de MapAtlas Geocoding API EU-specifieke adresformaten?
Ja. De API verwerkt correct het Duitse (huisnummer na straatnaam), Franse (huisnummer voor straatnaam), Nederlandse (4+2 postcodes) en andere EU-landadresconventies. Zoekopdrachten in de lokale taal leveren betere resultaten op dan getranslitereerde of Engelstalige zoekopdrachten.

