Toda organización que lleva más de unos años recopilando datos de direcciones tiene el mismo problema: una tabla de direcciones grande y desordenada con calidad desconocida. Direcciones introducidas a través de formularios web por usuarios que no prestaban atención. Registros migrados desde un CRM heredado que no validaba la entrada. Importaciones masivas de una hoja de Excel de un socio en un formato incompatible. Ubicaciones de negocios que se han trasladado desde que se recopilaron los datos.
Operar la logística con datos de direcciones deficientes genera entregas fallidas. Usarlos para marketing desperdicia el presupuesto de correo directo. Realizar valoraciones inmobiliarias con ellos produce análisis incorrectos de áreas de influencia. Antes de que se produzca cualquiera de esas operaciones, las direcciones deben limpiarse y validarse.
La geocodificación es la forma más efectiva de hacerlo a escala. Envíe una cadena de dirección sin procesar a la API de geocodificación y recibirá de vuelta componentes estructurados, coordenadas geográficas y una puntuación de confianza. Las direcciones que se geocodifican correctamente con alta confianza son casi con certeza válidas. Las que devuelven una confianza baja o ninguna coincidencia requieren revisión humana.
Este tutorial construye un script completo de Python que lee un CSV de direcciones, geocodifica cada una contra la API de geocodificación de MapAtlas, escribe los resultados con puntuaciones de confianza y marca los registros de baja confianza para revisión manual. Gestiona la limitación de velocidad, los errores de red transitorios y las peculiaridades del formato de direcciones europeo que los tutoriales de geocodificación centrados en EE.UU. pasan por alto.
Por qué los datos de direcciones se deterioran
La calidad de las direcciones se degrada por razones predecibles:
Errores de entrada manual. Los usuarios de formularios web escriben rápido, el corrector automático deforma los nombres de calles y la validación que acepta cualquier cadena no vacía deja pasar datos erróneos. Un estudio de datos de checkout B2C encontró que entre el 7 y el 12% de las direcciones introducidas manualmente contienen errores suficientemente graves como para causar un fallo en la entrega.
Traslados de negocios. Una base de datos B2B recopilada hace dos años tendrá aproximadamente un 10-15% de direcciones que ya no coinciden con la ubicación actual del negocio debido a mudanzas, fusiones y cierres.
Inconsistencias de formato. Los datos recopilados de múltiples fuentes utilizan convenciones diferentes: nombres de países completos vs. códigos ISO, "Calle" vs. "C/", número antes de la calle vs. después, "apartamento" vs. "piso" vs. "wohnung". Un geocodificador normaliza todos estos en una salida estructurada.
Migraciones de sistemas heredados. Los campos de dirección que estaban distribuidos en varias columnas de base de datos a menudo se concatenan durante la migración, perdiendo su estructura. Los campos de dirección de texto libre de sistemas más antiguos pueden incluir notas, referencias o formato que no forma parte de la dirección real.
El enfoque de geocodificación gestiona todos estos casos porque se basa en la coincidencia geográfica, no en la coincidencia de cadenas de texto. Una dirección con formato incorrecto puede geocodificarse correctamente si los datos de ubicación subyacentes coinciden.
Comprensión de las puntuaciones de confianza
La API de geocodificación de MapAtlas devuelve una propiedad confidence en cada resultado, con un rango de 0.0 a 1.0. Representa en qué medida el resultado devuelto coincide con la consulta de entrada, teniendo en cuenta las diferencias de formato, las abreviaturas y la ambigüedad.
| Confianza | Interpretación | Acción recomendada |
|---|---|---|
| 0.90 – 1.00 | Coincidencia exacta o casi exacta | Aceptar automáticamente |
| 0.85 – 0.89 | Coincidencia sólida, diferencias menores de formato | Aceptar con registro |
| 0.60 – 0.84 | Coincidencia parcial, calle encontrada pero número incierto | Marcar para revisión manual |
| 0.40 – 0.59 | Ambiguo, localidad coincidente pero no dirección específica | Rechazar o escalar |
| 0.00 – 0.39 | Sin coincidencia significativa | Rechazar, probablemente inválido |
Estos umbrales son puntos de partida. Para una operación logística donde las entregas fallidas son costosas, ajuste el umbral de aceptación automática a 0.92 o superior. Para una lista de correo de marketing donde el coste de la revisión manual supera el coste de algunas direcciones incorrectas, puede bajarlo a 0.80.
La API también devuelve una propiedad match_type que indica qué nivel de la jerarquía de direcciones coincidió: point (edificio exacto), interpolated (posición estimada entre números de casa conocidos), street (calle encontrada pero no el número) o locality (solo coincide la ciudad).
El script de validación de Python
Instale las dependencias:
pip install requests pandas tqdm
El script lee un CSV con una columna address (o columnas separadas street, city, country), geocodifica cada fila y escribe un nuevo CSV con los resultados de validación añadidos.
#!/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()
Ejecútelo:
python bulk_geocode.py
Para un CSV de 10.000 direcciones a 5 solicitudes/segundo, el script se completa en aproximadamente 35 minutos. La barra de progreso (via tqdm) muestra el rendimiento en tiempo real y el tiempo estimado de finalización.
Procesamiento del resultado
El CSV de salida agrega seis columnas a sus datos de entrada:
address, ..., geocoded_label, geocoded_lat, geocoded_lng, confidence, match_type, status
Filtre por estado para generar tres archivos de salida:
# 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")
El archivo addresses_review.csv es el que necesita atención humana. Patrones de revisión típicos:
- Tipo de coincidencia
street(confianza 0.65-0.80): Se encontró la calle pero no el número. Probablemente un edificio nuevo, una dirección rural con cobertura escasa o un error tipográfico en el número. Verifique la fuente original. - Tipo de coincidencia
locality(confianza 0.50-0.65): Solo coincidió la ciudad. El nombre de la calle probablemente está mal escrito o no existe en los datos. Busque la dirección en un directorio postal. - Baja confianza en direcciones de aspecto claramente válido: Compruebe si hay discrepancia entre el país y el idioma. Una dirección holandesa consultada sin restricción de país puede geocodificarse contra una ciudad alemana o belga con nombre similar.
Particularidades de las direcciones europeas
Las guías de geocodificación masiva escritas para el mercado estadounidense omiten las diferencias de formato que causan problemas en los conjuntos de datos de la UE. Estos son los problemas más comunes:
Alemania, orden del número de casa. Las direcciones alemanas usan el formato {calle} {número}: Berliner Straße 42. Muchos CRMs almacenan las direcciones en formato {número} {calle} porque fueron construidos para la convención de Reino Unido y EE.UU. Si sus direcciones alemanas se geocodifican con baja confianza, pruebe invirtiendo el número y el nombre de la calle en la cadena de consulta antes de enviarla.
Francia, arrondissements. Las direcciones de París incluyen un arrondissement (del 1.º al 20.º) como parte del código postal: 75001 a 75020. Las consultas que omiten el arrondissement y solo usan Paris se geocodifican al centroide de la ciudad, no al distrito específico. Esto aparece como una coincidencia locality con baja confianza en lugar de una coincidencia address.
Países Bajos, formato de código postal. Los códigos postales neerlandeses siguen un patrón estricto DDDD LL (4 dígitos, espacio, 2 letras mayúsculas). Los códigos almacenados sin el espacio (1012LG en lugar de 1012 LG) o con letras minúsculas se geocodificarán correctamente, pero si está validando el formato del código postal por separado, normalícelos a mayúsculas con espacio.
Bélgica, ambigüedad lingüística. Algunos municipios belgas tienen nombres diferentes en francés y neerlandés (Liège/Luik, Gent/Gand). La API gestiona ambos, pero la nomenclatura inconsistente en su conjunto de datos (algunos registros en francés, otros en neerlandés) puede producir distintos niveles de confianza. Normalice a un idioma por región antes de geocodificar.
España e Italia, variaciones de prefijos de calle. "Calle", "Carrer", "Via", "Viale" son todos prefijos de tipo de calle válidos y pueden aparecer abreviados ("C/") en los registros. El geocodificador gestiona las abreviaturas comunes, pero las abreviaciones poco habituales de sistemas heredados pueden requerir un paso de normalización previo.
Aumento del rendimiento con solicitudes concurrentes
El script secuencial es seguro y simple, pero lento. Para conjuntos de datos más grandes, reemplace el bucle secuencial con un executor concurrente:
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 trabajadores concurrentes a 5 RPS por trabajador, el rendimiento alcanza aproximadamente 50 solicitudes/segundo, lo que procesa 10.000 direcciones en menos de 4 minutos. Verifique el límite de solicitudes concurrentes de su plan MapAtlas antes de aumentar max_workers.
Para organizaciones que ejecutan la validación de direcciones como una operación recurrente, limpiando nuevas importaciones de CRM semanalmente o validando direcciones de entrega por la noche, consulte la página de soluciones de logística y entrega para ver cómo las integraciones de MapAtlas encajan en los flujos de trabajo operativos.
Si está creando autocompletado de direcciones para evitar que entren datos incorrectos en su sistema desde el principio (detectando errores en el origen en lugar de limpiarlos en lote), API de autocompletado de direcciones: cómo un solo campo mejora la conversión en checkout un 35% cubre la implementación frontend.
Trabajo con las coordenadas de salida
El resultado validado incluye geocoded_lat y geocoded_lng para cada dirección aceptada. Estas coordenadas abren capacidades de análisis que no eran posibles con cadenas de direcciones sin procesar:
- Cálculos de distancia. Calcule distancias en línea recta entre un almacén y cada dirección de entrega para estimar los niveles de coste de envío.
- Análisis de área de influencia. Represente en un mapa las ubicaciones de clientes validadas para ver dónde se concentra la demanda geográficamente.
- Asignación de zonas de entrega. Asigne cada dirección a una zona de entrega comprobando si sus coordenadas caen dentro de un polígono de zona.
- Detección de duplicados. Dos registros con las mismas coordenadas (con pocos metros de diferencia) son probablemente duplicados, incluso si las cadenas de dirección difieren en el formato.
Para la consistencia NAP (Nombre/Dirección/Teléfono) y su impacto en la visibilidad en búsquedas de IA, especialmente relevante para bases de datos de direcciones de negocios locales, Consistencia NAP para búsqueda de IA: por qué las direcciones discrepantes arruinan tu visibilidad en ChatGPT explica por qué las direcciones validadas por geocodificación son la base correcta para el marcado de datos estructurados.
Resumen
La geocodificación masiva con la API de geocodificación de MapAtlas le proporciona:
- Direcciones validadas con puntuaciones de confianza para saber qué registros aceptar automáticamente y cuáles necesitan revisión.
- Componentes estructurados (calle, número, código postal, ciudad, país) normalizados desde cualquier formato de entrada.
- Coordenadas geográficas para cada dirección aceptada, que permiten el análisis espacial y el enrutamiento.
- Soporte de formatos de direcciones de la UE integrado en la API, no algo que deba gestionar en el preprocesamiento.
El script de Python funciona a 5 direcciones/segundo de forma secuencial, y hasta 50/segundo con trabajadores concurrentes. Para 10.000 direcciones, calcule entre 4 y 40 minutos según la concurrencia. El resultado es un CSV limpio con tres niveles: aceptar, revisar, rechazar.
Regístrese para obtener una clave de API de MapAtlas gratuita para comenzar. La API de geocodificación admite consultas masivas en todos los planes; consulte pricing para ver las tarifas por solicitud y los límites del nivel gratuito mensual.

