Skip to main content
Tutorials

Validação em massa de endereços com a Geocoding API: 10.000 endereços de uma só vez

Validação em massa de endereços em Python com a Geocoding API da MapAtlas: rate limiting, retries, pontuação de confiança e formatos de endereço da UE para 10.000+ registros.

Brent van der Heiden13 min read
#bulk geocoding#geocoding api#address validation#python geocoding#batch geocoding#address cleaning
Validação em massa de endereços com a Geocoding API: 10.000 endereços de uma só vez

Toda organização que coleta endereços há mais de alguns anos tem o mesmo problema: uma tabela de endereços grande, bagunçada e de qualidade desconhecida. Endereços digitados em formulários web por usuários distraídos. Registros migrados de um CRM legado que não validava a entrada. Importações em massa da planilha Excel de um parceiro, em um formato incompatível. Empresas que mudaram de endereço depois que os dados foram coletados.

Operar a logística com endereços ruins gera entregas que falham. Fazer marketing com eles desperdiça verba de mala direta. Fazer avaliações imobiliárias com eles produz análises de área de influência incorretas. Antes de qualquer uma dessas operações, os endereços precisam ser limpos e validados.

A geocodificação é a forma mais eficaz de fazer isso em escala. Envie o texto bruto de um endereço para a Geocoding API e receba de volta componentes estruturados, coordenadas geográficas e uma pontuação de confiança. Endereços geocodificados com confiança alta são quase certamente válidos. Endereços que retornam confiança baixa, ou nenhuma correspondência, precisam de revisão humana.

Neste tutorial, você vai montar um script Python completo que lê um CSV de endereços, geocodifica cada um na Geocoding API da MapAtlas, grava os resultados com as pontuações de confiança e marca os registros de baixa confiança para revisão manual. O script trata rate limiting, erros de rede transitórios e as particularidades dos formatos de endereço europeus que os tutoriais de geocodificação focados nos EUA deixam de lado.

Por que os dados de endereço se deterioram

A qualidade dos endereços cai por motivos previsíveis:

Erros de digitação. Quem preenche formulários web digita rápido, o corretor automático estraga nomes de ruas e uma validação que aceita qualquer texto não vazio deixa passar lixo. Nos testes de checkout em larga escala do Baymard Institute, 9% dos endereços de entrega digitados manualmente continham erros de digitação ou de grafia.

Mudanças de endereço das empresas. Empresas mudam de endereço, se fundem e fecham o tempo todo. O Eurostat aponta uma taxa de mortalidade de empresas na UE de 8,5% em 2023, sem contar as mudanças de endereço, então uma base B2B coletada há dois anos terá uma parcela considerável de endereços que já não correspondem a uma empresa ativa.

Formatos inconsistentes. Dados coletados de várias fontes seguem convenções diferentes: nome completo do país ou código ISO, "St." ou "Street", número antes ou depois da rua, "flat", "apt" ou "wohnung". Um geocodificador normaliza tudo isso em uma saída estruturada.

Migrações de sistemas legados. Campos de endereço divididos em várias colunas do banco de dados costumam ser concatenados durante a migração, e a estrutura se perde. Campos de endereço em texto livre de sistemas antigos podem incluir observações, referências ou formatações que não fazem parte do endereço em si.

A geocodificação resolve todos esses casos porque compara localizações geográficas, não textos. Um endereço formatado de forma incorreta ainda pode ser geocodificado corretamente se os dados de localização por trás dele corresponderem.

Como funcionam as pontuações de confiança

A Geocoding API da MapAtlas retorna uma propriedade confidence em cada feature, com valores de 0,0 a 1,0. Ela indica o quanto o resultado corresponde à consulta de entrada, levando em conta diferenças de formato, abreviações e ambiguidades.

ConfiançaInterpretaçãoAção recomendada
0,90 a 1,00Correspondência exata ou quase exataAceitar automaticamente
0,85 a 0,89Correspondência forte, pequenas diferenças de formatoAceitar e registrar em log
0,60 a 0,84Correspondência parcial, rua encontrada mas número incertoMarcar para revisão manual
0,40 a 0,59Ambígua, cidade encontrada mas não o endereço específicoRejeitar ou escalar
0,00 a 0,39Nenhuma correspondência relevanteRejeitar, provavelmente inválido

Esses limites são um ponto de partida. Em uma operação logística em que entregas que falham custam caro, suba o limite de aceitação automática para 0,92 ou mais. Em uma lista de mala direta em que uma revisão manual custa mais do que alguns endereços ruins, reduza para 0,80.

A API também retorna uma propriedade match_type: exact (o endereço completo foi encontrado), interpolated (posição estimada entre números conhecidos) ou fallback (o endereço completo não foi encontrado e houve correspondência em um nível mais amplo). A propriedade layer informa qual foi esse nível, por exemplo address, street ou locality, e accuracy indica se o ponto é um point exato ou um centroid.

O script de validação em Python

Instale as dependências:

pip install requests pandas tqdm

O script lê um CSV com uma coluna address (ou com colunas separadas street, city e country), geocodifica cada linha e grava um novo CSV com os resultados da validação acrescentados.

#!/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

# ── Configuração ───────────────────────────────────────────────────────────────
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          # Requisições por segundo (respeite os limites do seu plano)
RETRY_ATTEMPTS  = 3          # Novas tentativas em erros transitórios
RETRY_DELAY_S   = 2.0        # Segundos entre tentativas

# Limites de confiança
ACCEPT_THRESHOLD = 0.85
REVIEW_THRESHOLD = 0.60

logging.basicConfig(level=logging.INFO, format='%(levelname)s %(message)s')
log = logging.getLogger(__name__)

# ── Função de geocodificação ───────────────────────────────────────────────────
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,
        '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:
                # Limite de requisições atingido, aguarde e tente de novo
                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))

    # Todas as tentativas esgotadas
    return {'geocoded_label': '', 'geocoded_lat': None, 'geocoded_lng': None,
            'confidence': 0.0, 'match_type': 'error', 'status': 'error'}


# ── Loop principal de processamento ────────────────────────────────────────────
def main():
    df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')

    # Monta o texto do endereço a partir das colunas disponíveis
    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)

    # Resumo
    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()

Execute o script:

python bulk_geocode.py

Para um CSV com 10.000 endereços a 5 requisições por segundo, o script precisa de pelo menos 33 minutos de pausas do rate limit, mais a latência de cada requisição, normalmente de 35 a 50 minutos no total. A barra de progresso (do tqdm) mostra a vazão em tempo real e o tempo estimado para terminar.

Como tratar a saída

O CSV de saída acrescenta seis colunas aos seus dados de entrada:

address, ..., geocoded_label, geocoded_lat, geocoded_lng, confidence, match_type, status

Filtre pela coluna status para gerar três arquivos de saída:

# Depois de executar main(), separe os endereços por nível de aceitação:
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")

O arquivo addresses_review.csv é o que precisa de atenção humana. Os padrões mais comuns na revisão são:

  • Fallback para a layer street (confiança de 0,65 a 0,80): a rua foi encontrada, mas o número não. Provavelmente é um prédio novo, um endereço rural com pouca cobertura ou um erro de digitação no número. Confira a fonte original.
  • Fallback para a layer locality (confiança de 0,50 a 0,65): só a cidade foi encontrada. O nome da rua provavelmente está escrito errado ou não existe nos dados. Consulte o endereço em um guia postal.
  • Confiança baixa em endereços que parecem válidos: verifique se há divergência de país ou de idioma. Um endereço holandês consultado sem restrição de país pode ser geocodificado para uma cidade alemã ou belga com nome parecido.

Particularidades dos endereços europeus

Os guias de geocodificação em massa escritos para o mercado americano ignoram as diferenças de formato que atrapalham conjuntos de dados europeus. Estes são os problemas mais comuns:

Alemanha, ordem do número. Endereços alemães usam o formato {street} {number}: Berliner Straße 42. Muitos CRMs guardam endereços no formato {number} {street} porque foram criados para a convenção do Reino Unido e dos EUA. Se os seus endereços alemães estão sendo geocodificados com confiança baixa, inverta o número e o nome da rua no texto da consulta antes de enviá-la.

França, arrondissements. Endereços de Paris incluem o arrondissement (do 1º ao 20º) no código postal: de 75001 a 75020. Consultas que omitem o arrondissement e usam apenas Paris são geocodificadas para o centroide da cidade, não para o distrito específico. Isso aparece como uma correspondência locality com confiança baixa, em vez de uma correspondência address.

Holanda, formato do código postal. Os códigos postais holandeses seguem o padrão rígido DDDD LL (4 dígitos, espaço, 2 letras maiúsculas). Códigos armazenados sem o espaço (1012LG em vez de 1012 LG) ou com letras minúsculas são geocodificados corretamente, mas, se você valida o formato do código postal separadamente, normalize para maiúsculas com espaço.

Bélgica, ambiguidade de idioma. Alguns municípios belgas têm nomes diferentes em francês e em holandês (Liège/Luik, Gent/Gand). A API aceita as duas formas, mas uma nomenclatura inconsistente no seu conjunto de dados (alguns registros em francês, outros em holandês) pode gerar níveis de confiança diferentes. Padronize um idioma por região antes de geocodificar.

Espanha e Itália, variações no tipo de via. "Calle", "Carrer", "Via" e "Viale" são prefixos de tipo de via válidos e podem aparecer abreviados ("C/") nos registros. O geocodificador reconhece as abreviações comuns, mas encurtamentos incomuns vindos de sistemas legados podem exigir uma etapa de normalização.

Mais vazão com requisições concorrentes

O script sequencial é seguro e simples, mas lento. Para conjuntos de dados maiores, troque o loop sequencial por um executor concorrente:

from concurrent.futures import ThreadPoolExecutor, as_completed

def main_concurrent(max_workers=10):
    df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')
    # ... (mesma preparação de antes) ...

    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()

    # ... (mesma gravação da saída de antes) ...

Com 10 workers concorrentes, a vazão é limitada pela latência da API e pelo rate limit do seu plano, e não por uma pausa: a 50 requisições por segundo, 10.000 endereços terminam em menos de 4 minutos. A versão concorrente não tem controle de vazão embutido, então adicione um ou reduza max_workers se o seu plano limitar as requisições por segundo. Confira o limite de requisições concorrentes do seu plano MapAtlas antes de aumentar max_workers.

Se a sua organização valida endereços de forma recorrente, limpando novas importações do CRM toda semana ou validando endereços de entrega toda noite, veja na página de soluções para logística e entregas como as integrações da MapAtlas se encaixam nos fluxos operacionais.

Se você está implementando autocompletar de endereço para impedir que dados ruins entrem no sistema desde o início (pegando os erros na origem em vez de limpá-los em lote), o artigo Address Autocomplete API: como um único campo aumenta a conversão do checkout explica a implementação no frontend.

Como usar as coordenadas da saída

A saída validada inclui geocoded_lat e geocoded_lng para cada endereço aceito. Essas coordenadas permitem análises que não eram possíveis com o texto bruto dos endereços:

  • Cálculo de distâncias. Calcule a distância em linha reta entre um armazém e cada endereço de entrega para estimar faixas de custo de frete.
  • Análise de área de influência. Plote as localizações validadas dos clientes em um mapa para ver onde a demanda se concentra.
  • Atribuição de zonas de entrega. Atribua cada endereço a uma zona de entrega testando se as coordenadas dele caem dentro do polígono da zona.
  • Detecção de duplicatas. Dois registros com as mesmas coordenadas (com diferença de poucos metros) provavelmente são duplicados, mesmo que o texto dos endereços esteja formatado de outro jeito.

Sobre consistência de NAP (nome, endereço e telefone) e o impacto dela na visibilidade na busca com IA, algo especialmente relevante para bases de endereços de negócios locais, o artigo Consistência NAP na busca com IA: por que endereços divergentes derrubam sua visibilidade no ChatGPT explica por que endereços validados por geocodificação são a base certa para a marcação de dados estruturados.

Resumo

A geocodificação em massa com a Geocoding API da MapAtlas oferece:

  • Endereços validados com pontuações de confiança, para você saber em quais registros confiar automaticamente e quais precisam de revisão.
  • Componentes estruturados (rua, número, código postal, cidade, país) normalizados a partir de qualquer formato de entrada.
  • Coordenadas geográficas para cada endereço aceito, o que viabiliza análise espacial e roteirização.
  • Suporte a formatos de endereço europeus já embutido na API, sem precisar tratar isso no pré-processamento.

O script Python processa 5 endereços por segundo no modo sequencial e até 50 por segundo com workers concorrentes. Para 10.000 endereços, reserve de 4 a 50 minutos, conforme a concorrência e a latência. O resultado é um CSV limpo com três níveis: aceitar, revisar e rejeitar.


Crie sua chave gratuita da API MapAtlas para começar. A Geocoding API aceita consultas em massa em todos os planos. Veja os preços para os valores por requisição e os limites mensais do plano gratuito.

Perguntas frequentes

Quanto tempo leva para geocodificar 10.000 endereços com a API da MapAtlas?

A 5 requisições por segundo, 10.000 endereços levam pelo menos 33 minutos só de pausas do rate limit, mais a latência de rede de cada requisição, então reserve de 35 a 50 minutos. Com 10 workers concorrentes a cerca de 50 requisições por segundo, o tempo cai para menos de 5 minutos, desde que o rate limit do seu plano permita. O script Python deste tutorial inclui as duas opções, sequencial e concorrente.

Qual limite de pontuação de confiança devo usar para validar endereços?

Um sistema prático de três níveis funciona bem: aceite endereços com confiança acima de 0,85, marque para revisão manual os que ficam entre 0,60 e 0,85 e rejeite os abaixo de 0,60. Os limites exatos dependem de quão crítica é a precisão do endereço no seu caso de uso: operações de logística devem usar limites mais rígidos do que campanhas de marketing.

A Geocoding API da MapAtlas lida com formatos de endereço específicos da UE?

Sim. A API interpreta corretamente os formatos alemão (número depois da rua), francês (número antes da rua), holandês (código postal 4+2) e as convenções de endereço de outros países da UE. Consultas no idioma local retornam resultados melhores do que consultas transliteradas ou no formato inglês.

Achou útil? Compartilhe.

Sobre o autor

Brent van der Heiden

Escrito por

Brent van der Heiden

Co-Founder & CEO at MapAtlas

Brent built MapAtlas out of a conviction that developers deserve location APIs with fair pricing and genuine end-user privacy. Before that he co-founded MapMetrics, a community mapping and navigation app. He writes about geospatial infrastructure, map data, AI search visibility, and how location data powers the products people rely on every day.

Ver todos os artigos →
Voltar ao blog