Ogni organizzazione che raccoglie dati sugli indirizzi da più di qualche anno ha lo stesso problema: una grande tabella di indirizzi disordinata con qualità sconosciuta. Indirizzi inseriti tramite moduli web da utenti disattenti. Record migrati da un CRM legacy che non validava l'input. Import in blocco da un foglio Excel di un partner in un formato incompatibile. Sedi aziendali che si sono spostate da quando i dati sono stati raccolti.
Eseguire la logistica su dati di indirizzi errati causa consegne fallite. Fare marketing con questi dati spreca il budget per la posta diretta. Effettuare valutazioni immobiliari produce analisi errate delle aree di catchment. Prima che queste operazioni avvengano, gli indirizzi devono essere puliti e validati.
La geocodifica è il modo più efficace per farlo su larga scala. Invia una stringa di indirizzo grezza all'API di geocodifica; ricevi in risposta componenti strutturati, coordinate geografiche e un punteggio di confidenza. Gli indirizzi che vengono geocodificati chiaramente con alta confidenza sono quasi certamente validi. Gli indirizzi che restituiscono una bassa confidenza o nessuna corrispondenza hanno bisogno di revisione umana.
Questo tutorial costruisce uno script Python completo che legge un CSV di indirizzi, geocodifica ognuno rispetto all'API di geocodifica MapAtlas, scrive i risultati con punteggi di confidenza e segnala i record a bassa confidenza per la revisione manuale. Gestisce la limitazione della velocità, gli errori di rete transitori e le peculiarità del formato degli indirizzi europei che i tutorial di geocodifica americani ignorano.
Perché i dati sugli indirizzi si deteriorano
La qualità degli indirizzi si degrada per ragioni prevedibili:
Errori di inserimento manuale. Gli utenti dei moduli web digitano velocemente, la correzione automatica stravolge i nomi delle strade e la validazione che accetta qualsiasi stringa non vuota lascia passare dati errati. Uno studio sui dati di checkout B2C ha rilevato che il 7-12% degli indirizzi inseriti manualmente contiene errori abbastanza significativi da causare il fallimento della consegna.
Trasferimenti aziendali. Un database B2B raccolto due anni fa avrà circa il 10-15% degli indirizzi che non corrispondono più alla sede aziendale attuale a causa di trasferimenti, fusioni e chiusure.
Incoerenze di formato. I dati raccolti da più fonti usano convenzioni diverse: nomi completi dei paesi vs. codici ISO, "St." vs. "Street", numero civico prima della via vs. dopo, "flat" vs. "apt" vs. "wohnung". Un geocoder normalizza tutto questo in output strutturato.
Migrazioni di sistemi legacy. I campi indirizzo divisi su più colonne del database vengono spesso concatenati durante la migrazione, perdendo struttura. I campi indirizzo in testo libero dei sistemi più vecchi possono includere note, riferimenti o formattazione che non fa parte dell'indirizzo effettivo.
L'approccio della geocodifica gestisce tutti questi casi perché si basa sulla corrispondenza geografica, non sulla corrispondenza di stringhe. Un indirizzo formattato in modo errato può comunque essere geocodificato correttamente se i dati di posizione sottostanti corrispondono.
Capire i punteggi di confidenza
L'API di geocodifica MapAtlas restituisce una proprietà confidence su ogni feature, che va da 0.0 a 1.0. Rappresenta quanto il risultato restituito corrisponde alla query di input, tenendo conto delle differenze di formato, abbreviazioni e ambiguità.
| Confidenza | Interpretazione | Azione consigliata |
|---|---|---|
| 0.90 - 1.00 | Corrispondenza esatta o quasi esatta | Accettare automaticamente |
| 0.85 - 0.89 | Corrispondenza forte, differenze minori di formato | Accettare con logging |
| 0.60 - 0.84 | Corrispondenza parziale, via trovata ma numero civico incerto | Segnalare per revisione manuale |
| 0.40 - 0.59 | Ambiguo, località corrispondente ma non indirizzo specifico | Rifiutare o escalare |
| 0.00 - 0.39 | Nessuna corrispondenza significativa | Rifiutare, probabilmente non valido |
Queste soglie sono punti di partenza. Per un'operazione logistica in cui le consegne fallite sono costose, inasprire la soglia di accettazione automatica a 0.92+. Per una lista di mailing marketing in cui il costo di una revisione manuale supera il costo di qualche indirizzo errato, ammorbidirla a 0.80.
L'API restituisce anche una proprietà match_type che indica a quale livello della gerarchia degli indirizzi è avvenuta la corrispondenza: point (edificio esatto), interpolated (posizione stimata tra numeri civici noti), street (via trovata ma non il numero civico) o locality (solo la città ha corrisposto).
Lo script Python di validazione
Installa le dipendenze:
pip install requests pandas tqdm
Lo script legge un CSV con una colonna address (o colonne separate street, city, country), geocodifica ogni riga e scrive un nuovo CSV con i risultati di validazione aggiunti.
#!/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()
Eseguilo:
python bulk_geocode.py
Per un CSV di 10.000 indirizzi a 5 richieste/secondo, lo script si completa in circa 35 minuti. La barra di avanzamento (tramite tqdm) mostra la velocità effettiva in tempo reale e il tempo stimato di completamento.
Gestire l'output
Il CSV di output aggiunge sei colonne ai tuoi dati di input:
address, ..., geocoded_label, geocoded_lat, geocoded_lng, confidence, match_type, status
Filtra per stato per generare tre file di output:
# 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")
Il file addresses_review.csv è quello che richiede attenzione umana. Pattern di revisione tipici:
- Tipo di corrispondenza
street(confidenza 0.65-0.80): La via è stata trovata ma non il numero civico. Probabilmente un edificio nuovo, un indirizzo rurale con copertura scarsa o un errore di battitura nel numero civico. Controlla la fonte originale. - Tipo di corrispondenza
locality(confidenza 0.50-0.65): Solo la città ha corrisposto. Il nome della via è probabilmente scritto in modo errato o non esiste nei dati. Cerca l'indirizzo in un elenco postale. - Bassa confidenza su indirizzi dall'aspetto chiaramente valido: Controlla la mancata corrispondenza paese/lingua. Un indirizzo olandese interrogato senza una restrizione di paese può essere geocodificato rispetto a una città tedesca o belga con nome simile.
Peculiarità degli indirizzi europei da conoscere
Le guide alla geocodifica in blocco scritte per il mercato statunitense ignorano le differenze di formato che intralciano i dataset europei. Ecco i problemi più comuni:
Germania, ordine del numero civico. Gli indirizzi tedeschi usano il formato {via} {numero}: Berliner Straße 42. Molti CRM memorizzano gli indirizzi nel formato {numero} {via} perché sono stati costruiti per la convenzione britannica/americana. Se i tuoi indirizzi tedeschi vengono geocodificati con bassa confidenza, prova a invertire il numero e il nome della via nella stringa di query prima dell'invio.
Francia, arrondissement. Gli indirizzi parigini includono un arrondissement (1-20) come parte del codice postale: da 75001 a 75020. Le query che omettono l'arrondissement e usano solo Paris vengono geocodificate al centroide della città, non al distretto specifico: questo appare come una corrispondenza locality con bassa confidenza piuttosto che una corrispondenza address.
Paesi Bassi, formato del codice postale. I codici postali olandesi seguono uno schema rigoroso DDDD LL (4 cifre, spazio, 2 lettere maiuscole). I codici memorizzati senza lo spazio (1012LG invece di 1012 LG) o con lettere minuscole vengono geocodificati correttamente, ma se stai validando separatamente il formato del codice postale, normalizza in maiuscolo con uno spazio.
Belgio, ambiguità linguistica. Alcuni comuni belgi hanno nomi diversi in francese e in olandese (Liège/Luik, Gent/Gand). L'API gestisce entrambi, ma la denominazione incoerente nel tuo dataset (alcuni record in francese, altri in olandese) può produrre livelli di confidenza diversi. Normalizza in una lingua per regione prima della geocodifica.
Spagna e Italia, variazioni dei prefissi stradali. "Calle", "Carrer", "Via", "Viale" sono tutti prefissi validi del tipo di strada e possono apparire abbreviati ("C/") nei record. Il geocoder gestisce le abbreviazioni comuni ma le shortenings insolite dai sistemi legacy potrebbero richiedere un passaggio di normalizzazione.
Aumentare la velocità con richieste concorrenti
Lo script sequenziale è sicuro e semplice ma lento. Per dataset più grandi, sostituisci il loop sequenziale con un executor concorrente:
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) ...
Con 10 worker concorrenti a 5 RPS per worker, la velocità raggiunge circa 50 richieste/secondo: 10.000 indirizzi in meno di 4 minuti. Controlla il limite di richieste concorrenti del tuo piano MapAtlas prima di aumentare max_workers.
Per le organizzazioni che eseguono la validazione degli indirizzi come operazione ricorrente, pulendo i nuovi import CRM settimanalmente o validando gli indirizzi di consegna ogni notte, consulta la pagina delle soluzioni Logistics & Delivery per come le integrazioni MapAtlas si adattano ai workflow operativi.
Se stai costruendo l'autocompletamento degli indirizzi per prevenire l'inserimento di dati errati nel sistema fin dall'inizio (intercettando gli errori alla fonte piuttosto che pulirli in batch), API di completamento automatico degli indirizzi: come un solo campo aumenta la conversione del checkout del 35% copre l'implementazione frontend.
Lavorare con l'output delle coordinate
L'output validato include geocoded_lat e geocoded_lng per ogni indirizzo accettato. Queste coordinate aprono capacità di analisi che non erano possibili con le stringhe di indirizzi grezze:
- Calcoli delle distanze. Calcola le distanze in linea retta tra un magazzino e ogni indirizzo di consegna per stimare le fasce di costo di spedizione.
- Analisi dell'area di catchment. Traccia le posizioni dei clienti validati su una mappa per vedere dove si concentra geograficamente la domanda.
- Assegnazione delle zone di consegna. Assegna ogni indirizzo a una zona di consegna verificando se le sue coordinate rientrano in un poligono di zona.
- Rilevamento dei duplicati. Due record con le stesse coordinate (entro pochi metri) sono probabilmente duplicati, anche se le stringhe di indirizzo differiscono nella formattazione.
Per la coerenza NAP (Name/Address/Phone) e il suo impatto sulla visibilità nella ricerca IA, particolarmente rilevante per i database di indirizzi delle attività locali, Coerenza NAP per la ricerca IA: perché gli indirizzi non corrispondenti azzerano la tua visibilità su ChatGPT spiega perché gli indirizzi validati tramite geocodifica sono la base giusta per il markup dei dati strutturati.
Riepilogo
La geocodifica in blocco con l'API di geocodifica MapAtlas ti fornisce:
- Indirizzi validati con punteggi di confidenza in modo da sapere quali record accettare automaticamente e quali necessitano di revisione.
- Componenti strutturati (via, numero civico, codice postale, città, paese) normalizzati da qualsiasi formato di input.
- Coordinate geografiche per ogni indirizzo accettato, abilitando analisi spaziali e routing.
- Supporto per il formato degli indirizzi europei integrato nell'API, non qualcosa che devi gestire nella pre-elaborazione.
Lo script Python gira a 5 indirizzi/secondo sequenzialmente, fino a 50/secondo con worker concorrenti. Per 10.000 indirizzi, prevedi 4-40 minuti a seconda della concorrenza. L'output è un CSV pulito con tre livelli: accetta, revisione, rifiuta.
Registrati per una chiave API MapAtlas gratuita per iniziare. L'API di geocodifica supporta query in blocco su tutti i piani: consulta i prezzi per le tariffe per richiesta e i limiti mensili del piano gratuito.

