Jede Organisation, die seit mehr als ein paar Jahren Adressdaten erfasst, hat dasselbe Problem: eine große, unordentliche Adresstabelle mit unbekannter Qualität. Adressen, die über Webformulare von unaufmerksamen Nutzern eingegeben wurden. Datensätze, die aus einem alten CRM migriert wurden, das keine Eingaben validierte. Massenimporte aus der Excel-Tabelle eines Partners in einem inkompatiblen Format. Unternehmensstandorte, die sich seit der Datenerfassung verändert haben.
Logistik mit fehlerhaften Adressdaten zu betreiben führt zu fehlgeschlagenen Lieferungen. Marketing damit zu betreiben verschwendet Direktmailing-Budget. Immobilienbewertungen damit durchzuführen liefert falsche Einzugsgebietsanalysen. Bevor diese Vorgänge stattfinden, müssen die Adressen bereinigt und validiert werden.
Geocoding ist der effektivste Weg, das in großem Maßstab zu tun. Senden Sie einen rohen Adressstring an die Geocoding API und erhalten Sie strukturierte Komponenten, geografische Koordinaten und einen Konfidenz-Score zurück. Adressen, die mit hoher Konfidenz sauber geocodiert werden, sind fast sicher gültig. Adressen, die eine niedrige Konfidenz oder gar keine Übereinstimmung zurückgeben, müssen manuell überprüft werden.
Dieses Tutorial erstellt ein vollständiges Python-Skript, das eine CSV-Datei mit Adressen liest, jede Adresse gegen die MapAtlas Geocoding API geocodiert, die Ergebnisse mit Konfidenz-Scores schreibt und Datensätze mit niedriger Konfidenz zur manuellen Überprüfung kennzeichnet. Es verarbeitet Ratenbegrenzungen, vorübergehende Netzwerkfehler und EU-Adressformat-Besonderheiten, die US-zentrierte Geocoding-Tutorials auslassen.
Warum Adressdaten schlechter werden
Adressqualität verschlechtert sich aus vorhersehbaren Gründen:
Manuelle Eingabefehler. Webformular-Nutzer tippen schnell, Autocorrect vermurkst Straßennamen, und eine Validierung, die jeden nicht-leeren String akzeptiert, lässt Fehler durch. In den groß angelegten Checkout-Tests des Baymard Institute enthielten 9 % der manuell eingegebenen Lieferadressen Tipp- oder Schreibfehler.
Unternehmensumzüge. Unternehmen ziehen um, fusionieren und schließen ständig. Eurostat beziffert die Sterberate von Unternehmen in der EU für 2023 auf 8,5 %, Umzüge noch nicht mitgerechnet. Eine vor zwei Jahren erfasste B2B-Datenbank enthält daher einen spürbaren Anteil an Adressen, die keinem aktiven Unternehmen mehr entsprechen.
Format-Inkonsistenzen. Daten aus mehreren Quellen verwenden unterschiedliche Konventionen: vollständige Ländernamen vs. ISO-Codes, "Str." vs. "Straße", Hausnummer vor oder nach dem Straßennamen, "Wohnung" vs. "App" vs. "Apt.". Ein Geocoder normalisiert all das in eine strukturierte Ausgabe.
Legacy-System-Migrationen. Adressfelder, die auf mehrere Datenbankspalten aufgeteilt waren, werden bei der Migration häufig verkettet und verlieren dabei die Struktur. Freitextadressfelder aus älteren Systemen können Notizen, Referenzen oder Formatierungen enthalten, die nicht Teil der eigentlichen Adresse sind.
Der Geocoding-Ansatz bewältigt all das, weil er auf geografischem Matching basiert, nicht auf String-Matching. Eine falsch formatierte Adresse kann trotzdem korrekt geocodiert werden, wenn die zugrunde liegenden Ortsdaten übereinstimmen.
Konfidenz-Scores verstehen
Die MapAtlas Geocoding API gibt eine confidence-Eigenschaft für jedes Feature zurück, die von 0,0 bis 1,0 reicht. Sie gibt an, wie gut das zurückgegebene Ergebnis mit der Eingabeabfrage übereinstimmt, unter Berücksichtigung von Formatunterschieden, Abkürzungen und Mehrdeutigkeiten.
| Konfidenz | Interpretation | Empfohlene Aktion |
|---|---|---|
| 0,90 bis 1,00 | Exakte oder nahezu exakte Übereinstimmung | Automatisch akzeptieren |
| 0,85 bis 0,89 | Starke Übereinstimmung, kleine Formatunterschiede | Mit Protokollierung akzeptieren |
| 0,60 bis 0,84 | Teilweise Übereinstimmung, Straße gefunden aber Hausnummer unsicher | Zur manuellen Überprüfung kennzeichnen |
| 0,40 bis 0,59 | Mehrdeutig, Ort gefunden aber nicht spezifische Adresse | Ablehnen oder eskalieren |
| 0,00 bis 0,39 | Keine sinnvolle Übereinstimmung | Ablehnen, wahrscheinlich ungültig |
Diese Schwellenwerte sind Ausgangspunkte. Für einen Logistikbetrieb, bei dem fehlgeschlagene Lieferungen teuer sind, sollte der Schwellenwert für automatische Akzeptanz auf 0,92+ erhöht werden. Für eine Marketing-Mailingliste, bei der die Kosten einer manuellen Überprüfung höher sind als die Kosten einiger schlechter Adressen, kann er auf 0,80 gesenkt werden.
Die API gibt außerdem eine match_type-Eigenschaft zurück: exact (die vollständige Adresse wurde gefunden), interpolated (Position zwischen bekannten Hausnummern geschätzt) oder fallback (die vollständige Adresse wurde nicht gefunden, stattdessen passte eine gröbere Ebene). Die Eigenschaft layer zeigt, welche Ebene das war, zum Beispiel address, street oder locality, und accuracy gibt an, ob der Punkt ein exakter point oder ein centroid ist.
Das Python-Validierungsskript
Abhängigkeiten installieren:
pip install requests pandas tqdm
Das Skript liest eine CSV-Datei mit einer address-Spalte (oder separaten street-, city-, country-Spalten), geocodiert jede Zeile und schreibt eine neue CSV-Datei mit angehängten Validierungsergebnissen.
#!/usr/bin/env python3
"""
bulk_geocode.py, Validiert eine CSV von Adressen über die MapAtlas Geocoding API.
Eingabe-CSV muss entweder haben:
- Eine 'address'-Spalte (vollständiger Adressstring), oder
- 'street', 'city' und 'country'-Spalten (werden verkettet)
Gibt eine neue CSV mit zusätzlichen Spalten aus:
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
# ── Konfiguration ──────────────────────────────────────────────────────────────
API_KEY = 'YOUR_API_KEY'
API_BASE = 'https://gateway.mapmetrics-atlas.net/forward-geocode/'
INPUT_CSV = 'addresses.csv'
OUTPUT_CSV = 'addresses_validated.csv'
RATE_LIMIT_RPS = 5 # Anfragen pro Sekunde
RETRY_ATTEMPTS = 3 # Wiederholungen bei vorübergehenden Fehlern
RETRY_DELAY_S = 2.0 # Sekunden zwischen Wiederholungen
# Konfidenz-Schwellenwerte
ACCEPT_THRESHOLD = 0.85
REVIEW_THRESHOLD = 0.60
logging.basicConfig(level=logging.INFO, format='%(levelname)s %(message)s')
log = logging.getLogger(__name__)
# ── Geocoding-Funktion ─────────────────────────────────────────────────────────
def geocode_address(address_str: str) -> dict:
"""
Geocodiert einen einzelnen Adressstring. Gibt ein Dict mit Ergebnisfeldern zurück.
Wiederholt bei Netzwerkfehlern und 429-Ratenbegrenzungsantworten.
"""
params = {
'text': address_str,
'token': 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:
wait = float(resp.headers.get('Retry-After', RETRY_DELAY_S * (attempt + 1)))
log.warning(f'Ratenbegrenzt. Warte {wait:.1f}s vor Wiederholung {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'Netzwerkfehler bei Versuch {attempt + 1}: {exc}')
if attempt < RETRY_ATTEMPTS - 1:
time.sleep(RETRY_DELAY_S * (attempt + 1))
return {'geocoded_label': '', 'geocoded_lat': None, 'geocoded_lng': None,
'confidence': 0.0, 'match_type': 'error', 'status': 'error'}
# ── Hauptverarbeitungsschleife ───────────────────────────────────────────────
def main():
df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')
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 muss 'address' oder 'street'+'city'+'country'-Spalten haben.")
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)
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── Ergebnisse ────────────────────────────────')
log.info(f'Gesamt verarbeitet : {total:,}')
log.info(f'Akzeptiert (>={ACCEPT_THRESHOLD}) : {accept:,} ({accept/total:.1%})')
log.info(f'Überprüfen : {review:,} ({review/total:.1%})')
log.info(f'Abgelehnt : {reject:,} ({reject/total:.1%})')
log.info(f'Fehler : {errors:,} ({errors/total:.1%})')
log.info(f'Ausgabe geschrieben nach {OUTPUT_CSV}')
if __name__ == '__main__':
main()
Ausführen:
python bulk_geocode.py
Für eine CSV mit 10.000 Adressen bei 5 Anfragen/Sekunde benötigt das Skript mindestens 33 Minuten reine Rate-Limit-Pause plus die Latenz jeder Anfrage, insgesamt typischerweise 35 bis 50 Minuten. Der Fortschrittsbalken (via tqdm) zeigt den Echtzeit-Durchsatz und die geschätzte Fertigstellungszeit.
Die Ausgabe verarbeiten
Die Ausgabe-CSV fügt Ihren Eingabedaten sechs Spalten hinzu:
address, ..., geocoded_label, geocoded_lat, geocoded_lng, confidence, match_type, status
Filtern Sie nach Status, um drei Ausgabedateien zu erstellen:
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"Sauber: {len(df[df['status']=='accept']):,} Adressen zur Verwendung bereit")
print(f"Überprüfen: {len(df[df['status']=='review']):,} Adressen für manuelle Prüfung")
print(f"Abgelehnt: {len(df[df['status']=='reject']):,} Adressen verwerfen oder korrigieren")
Die Datei addresses_review.csv ist diejenige, die menschliche Aufmerksamkeit erfordert. Typische Überprüfungsmuster:
- Fallback auf Layer
street(Konfidenz 0,65 bis 0,80): Die Straße wurde gefunden, aber nicht die Hausnummer. Wahrscheinlich ein neues Gebäude, eine ländliche Adresse mit geringer Abdeckung oder ein Tippfehler in der Hausnummer. - Fallback auf Layer
locality(Konfidenz 0,50 bis 0,65): Nur die Stadt stimmte überein. Der Straßenname ist wahrscheinlich falsch geschrieben oder existiert in den Daten nicht. - Niedrige Konfidenz bei eindeutig gültig aussehenden Adressen: Überprüfen Sie auf Länder-/Sprach-Mismatch. Eine niederländische Adresse ohne Länderbeschränkung kann gegen eine ähnlich benannte deutsche oder belgische Stadt geocodiert werden.
EU-Adressbesonderheiten
Deutschland, Hausnummernreihenfolge. Deutsche Adressen verwenden das Format {Straße} {Nummer}: Berliner Straße 42. Viele CRMs speichern Adressen im Format {Nummer} {Straße}. Wenn Ihre deutschen Adressen mit niedriger Konfidenz geocodiert werden, versuchen Sie, Nummer und Straßenname in der Abfragezeichenfolge umzukehren.
Frankreich, Arrondissements. Pariser Adressen enthalten ein Arrondissement als Teil der Postleitzahl: 75001 bis 75020. Abfragen ohne Arrondissement werden zum Stadtzentrum geocodiert, nicht zum spezifischen Bezirk.
Niederlande, Postleitzahlformat. Niederländische Postleitzahlen folgen dem strikten Muster DDDD LL (4 Ziffern, Leerzeichen, 2 Großbuchstaben). Codes ohne Leerzeichen werden korrekt geocodiert, aber normalisieren Sie das Format für separate Postleitzahl-Validierungen.
Belgien, Sprachambiguität. Einige belgische Gemeinden haben unterschiedliche französische und niederländische Namen (Liège/Luik, Gent/Gand). Normalisieren Sie vor dem Geocoding auf eine Sprache pro Region.
Spanien und Italien, Straßenpräfix-Variationen. "Calle", "Carrer", "Via", "Viale" können abgekürzt erscheinen. Der Geocoder verarbeitet gängige Abkürzungen, aber ungewöhnliche Kurzformen aus Legacy-Systemen können einen Normalisierungsschritt erfordern.
Durchsatz mit gleichzeitigen Anfragen erhöhen
Das sequentielle Skript ist sicher und einfach, aber langsam. Für größere Datensätze ersetzen Sie die sequentielle Schleife durch einen gleichzeitigen Executor:
from concurrent.futures import ThreadPoolExecutor, as_completed
def main_concurrent(max_workers=10):
df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')
# ... (dieselbe Einrichtung wie zuvor) ...
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()
# ... (dieselbe Ausgabeschreibung wie zuvor) ...
Mit 10 gleichzeitigen Workern begrenzen die API-Latenz und das Rate Limit Ihres Plans den Durchsatz, nicht eine Pause im Code: Bei 50 Anfragen pro Sekunde sind 10.000 Adressen in unter 4 Minuten fertig. Die parallele Variante hat keine eingebaute Drosselung. Bauen Sie also eine ein oder senken Sie max_workers, wenn Ihr Plan die Anfragen pro Sekunde begrenzt. Überprüfen Sie das Limit für gleichzeitige Anfragen Ihres MapAtlas-Plans, bevor Sie max_workers erhöhen.
Für Organisationen, die Adressvalidierung als wiederkehrenden Vorgang durchführen, beschreibt die Lösungsseite Logistik und Lieferung, wie MapAtlas-Integrationen in betriebliche Abläufe passen.
Wenn Sie Adress-Autocomplete einbauen möchten, um fehlerhafte Daten von vornherein zu verhindern, behandelt Adress-Autocomplete-API: Wie ein Feld die Checkout-Konversion steigert die Frontend-Implementierung.
Mit der Koordinaten-Ausgabe arbeiten
Die validierte Ausgabe enthält geocoded_lat und geocoded_lng für jede akzeptierte Adresse. Diese Koordinaten ermöglichen Analysen, die mit rohen Adressstrings nicht möglich waren:
- Entfernungsberechnungen. Berechnen Sie Luftlinienentfernungen zwischen einem Lager und jeder Lieferadresse.
- Einzugsgebietsanalyse. Tragen Sie validierte Kundenstandorte auf einer Karte ein, um geografische Nachfragekonzentration zu erkennen.
- Lieferzonen-Zuweisung. Weisen Sie jeder Adresse eine Lieferzone zu, indem Sie testen, ob ihre Koordinaten innerhalb eines Zonenpolygons liegen.
- Duplikaterkennung. Zwei Einträge mit denselben Koordinaten (innerhalb weniger Meter) sind wahrscheinlich Duplikate, auch wenn die Adressstrings unterschiedlich formatiert sind.
Für NAP-Konsistenz und deren Auswirkung auf die KI-Suchsichtbarkeit erklärt NAP Consistency for AI Search: Why Mismatched Addresses Kill Your ChatGPT Visibility, warum geocoding-validierte Adressen die richtige Grundlage für strukturierte Datenmarkierung sind.
Zusammenfassung
Massen-Geocoding mit der MapAtlas Geocoding API liefert Ihnen:
- Validierte Adressen mit Konfidenz-Scores, sodass Sie wissen, welchen Einträgen Sie automatisch vertrauen können.
- Strukturierte Komponenten aus jedem Eingabeformat normalisiert.
- Geografische Koordinaten für jede akzeptierte Adresse, die Raumanalyse und Routing ermöglichen.
- EU-Adressformat-Unterstützung in der API integriert.
Das Python-Skript läuft sequentiell bei 5 Adressen/Sekunde, bis zu 50/Sekunde mit gleichzeitigen Workern. Für 10.000 Adressen kalkulieren Sie je nach Parallelität und Latenz 4 bis 50 Minuten ein. Die Ausgabe ist eine saubere CSV mit drei Ebenen: akzeptieren, überprüfen, ablehnen.
Melden Sie sich für einen kostenlosen MapAtlas API-Key an um zu starten. Die Geocoding API unterstützt Massenabfragen in allen Plänen, Preise und monatliche Gratisstufenlimits finden Sie auf der Preisseite.
Häufig gestellte Fragen
Wie lange dauert das Geocoding von 10.000 Adressen mit der MapAtlas API?
Bei 5 Anfragen pro Sekunde brauchen 10.000 Adressen mindestens 33 Minuten reine Rate-Limit-Pause, dazu kommt die Netzwerklatenz jeder Anfrage. Planen Sie also 35 bis 50 Minuten ein. Mit 10 parallelen Workern bei rund 50 Anfragen pro Sekunde sinkt die Dauer auf unter 5 Minuten, sofern das Rate Limit Ihres Plans das zulässt. Das Python-Skript in diesem Tutorial enthält eine sequenzielle und eine parallele Variante.
Welchen Confidence-Schwellenwert sollte ich für die Adressvalidierung verwenden?
Bewährt hat sich ein dreistufiges System: Adressen mit einem Confidence-Wert über 0,85 akzeptieren, Werte zwischen 0,60 und 0,85 zur manuellen Prüfung markieren und Werte unter 0,60 verwerfen. Die genauen Schwellenwerte hängen davon ab, wie kritisch die Adressgenauigkeit für Ihren Anwendungsfall ist. Logistikprozesse brauchen strengere Schwellenwerte als Marketingkampagnen.
Unterstützt die MapAtlas Geocoding API EU-spezifische Adressformate?
Ja. Die API parst deutsche (Hausnummer nach der Straße), französische (Hausnummer vor der Straße), niederländische (Postleitzahlen im Format 4+2) und weitere EU-Adresskonventionen korrekt. Anfragen in der Landessprache liefern bessere Ergebnisse als transliterierte oder englisch formatierte Anfragen.

