Skip to main content
Массовая валидация адресов с Geocoding API: проверка 10 000 адресов за один проход
Tutorials

Массовая валидация адресов с Geocoding API: проверка 10 000 адресов за один проход

Массовая валидация адресов на Python через MapAtlas Geocoding API: rate limiting, retry-логика, confidence score и форматы адресов ЕС для 10 000+ записей.

Brent van der Heiden11 min read
#bulk geocoding#geocoding api#address validation#python geocoding#batch geocoding#address cleaning

У любой организации, которая собирает адреса дольше нескольких лет, одна и та же проблема: большая, неупорядоченная таблица адресов неизвестного качества. Адреса, которые пользователи наспех вводили в веб-формы. Записи, перенесённые из старой CRM без проверки ввода. Массовый импорт из Excel-таблицы партнёра в несовместимом формате. Компании, которые переехали с момента сбора данных.

Логистика на плохих адресах приводит к сорванным доставкам. Маркетинг на них расходует бюджет на прямую почтовую рассылку впустую. Оценка недвижимости на таких данных даёт неверный анализ зон охвата. Прежде чем запускать любой из этих процессов, адреса нужно очистить и проверить.

Геокодирование остаётся самым эффективным способом сделать это в больших объёмах. Отправьте сырую строку адреса в Geocoding API и получите в ответ структурированные компоненты, географические координаты и оценку уверенности (confidence). Адреса, которые геокодируются с высокой уверенностью, почти наверняка корректны. Адреса с низкой уверенностью или без совпадения нужно проверить вручную.

В этом туториале создаётся полноценный Python-скрипт: он читает CSV с адресами, геокодирует каждый через MapAtlas Geocoding API, записывает результаты с оценками уверенности и помечает записи с низкой уверенностью для ручной проверки. Скрипт учитывает ограничения частоты запросов, временные сетевые ошибки и особенности форматов адресов в ЕС, которые пропускают туториалы, ориентированные на США.

Почему адресные данные портятся

Качество адресов ухудшается по предсказуемым причинам.

Ошибки ручного ввода. Пользователи веб-форм печатают быстро, автозамена искажает названия улиц, а проверка, которая пропускает любую непустую строку, пропускает и мусор. В масштабном тестировании оформления заказа Baymard Institute 9% вручную введённых адресов доставки содержали опечатки или орфографические ошибки.

Переезды компаний. Компании постоянно переезжают, сливаются и закрываются. По данным Eurostat, уровень ликвидации предприятий в ЕС в 2023 году составил 8,5%, и это без учёта переездов, поэтому в B2B-базе, собранной два года назад, заметная доля адресов уже не соответствует действующим компаниям.

Разнобой в форматах. Данные из разных источников оформлены по разным правилам: полные названия стран или ISO-коды, «St.» или «Street», номер дома до улицы или после, «flat», «apt» или «wohnung». Геокодер приводит всё это к единому структурированному виду.

Миграции старых систем. Поля адреса, которые хранились в нескольких колонках базы данных, при миграции часто склеиваются в одну строку и теряют структуру. Свободные текстовые поля адреса из старых систем могут содержать заметки, ссылки или форматирование, которые не являются частью адреса.

Геокодирование справляется со всеми этими случаями, потому что опирается на географическое сопоставление, а не на сравнение строк. Адрес с неправильным форматированием всё равно может геокодироваться верно, если совпадают данные о самом месте.

Как устроены оценки уверенности

MapAtlas Geocoding API возвращает у каждого объекта свойство confidence со значением от 0,0 до 1,0. Оно показывает, насколько результат соответствует входному запросу, с учётом различий в формате, сокращений и неоднозначности.

УверенностьИнтерпретацияРекомендуемое действие
0,90-1,00Точное или почти точное совпадениеПринимать автоматически
0,85-0,89Надёжное совпадение, небольшие различия в форматеПринимать с записью в лог
0,60-0,84Частичное совпадение, улица найдена, номер дома под вопросомОтправить на ручную проверку
0,40-0,59Неоднозначно, совпал населённый пункт, но не конкретный адресОтклонить или передать дальше
0,00-0,39Осмысленного совпадения нетОтклонить, скорее всего адрес неверный

Эти пороги служат отправной точкой. Для логистики, где сорванная доставка стоит дорого, поднимите порог автоматического принятия до 0,92 и выше. Для маркетинговой рассылки, где ручная проверка обходится дороже нескольких ошибочных адресов, снизьте его до 0,80.

API также возвращает свойство match_type: exact (адрес совпал полностью), interpolated (положение рассчитано между известными номерами домов) или fallback (полный адрес не найден, и вместо него совпал более общий уровень). Свойство layer показывает, какой именно это уровень, например address, street или locality, а accuracy сообщает, точная ли это точка (point) или центроид (centroid).

Python-скрипт для валидации

Установите зависимости:

pip install requests pandas tqdm

Скрипт читает CSV с колонкой address (или с отдельными колонками street, city, country), геокодирует каждую строку и записывает новый CSV с добавленными результатами проверки.

#!/usr/bin/env python3
"""
bulk_geocode.py: проверка CSV с адресами через MapAtlas Geocoding API.

Во входном CSV должна быть:
  - колонка 'address' (полная строка адреса) или
  - колонки 'street', 'city' и 'country' (они будут объединены)

На выходе создаётся новый CSV с дополнительными колонками:
  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

# ── Настройки ──────────────────────────────────────────────────────────────────
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          # Запросов в секунду (не превышайте лимиты тарифа)
RETRY_ATTEMPTS  = 3          # Повторы при временных ошибках
RETRY_DELAY_S   = 2.0        # Пауза между повторами в секундах

# Пороги уверенности
ACCEPT_THRESHOLD = 0.85
REVIEW_THRESHOLD = 0.60

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

# ── Функция геокодирования ─────────────────────────────────────────────────────
def geocode_address(address_str: str) -> dict:
    """
    Геокодирует одну строку адреса и возвращает словарь с полями результата.
    Повторяет запрос при сетевых ошибках и ответах 429 (превышение лимита).
    """
    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'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))

    # Все повторы исчерпаны
    return {'geocoded_label': '', 'geocoded_lat': None, 'geocoded_lng': None,
            'confidence': 0.0, 'match_type': 'error', 'status': 'error'}


# ── Основной цикл обработки ────────────────────────────────────────────────────
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 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)

    # Сводка
    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()

Запустите скрипт:

python bulk_geocode.py

Для CSV на 10 000 адресов при 5 запросах в секунду скрипту нужно не меньше 33 минут одних пауз для соблюдения лимита плюс задержка каждого запроса, обычно 35-50 минут в сумме. Индикатор прогресса (через tqdm) показывает текущую скорость и расчётное время завершения.

Работа с результатами

Выходной CSV добавляет к исходным данным шесть колонок:

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

Отфильтруйте записи по статусу и получите три файла:

# После запуска main() разделяем результаты по уровням:
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")

Внимания человека требует файл addresses_review.csv. Типичные случаи при проверке:

  • Откат к слою street (уверенность 0,65-0,80): улица найдена, а номер дома нет. Скорее всего, это новое здание, сельский адрес с неполным покрытием или опечатка в номере дома. Сверьтесь с исходным источником.
  • Откат к слою locality (уверенность 0,50-0,65): совпал только город. Название улицы, вероятно, написано с ошибкой или отсутствует в данных. Найдите адрес в почтовом справочнике.
  • Низкая уверенность у адресов, которые выглядят корректными: проверьте несоответствие страны или языка. Нидерландский адрес без ограничения по стране может геокодироваться в немецкий или бельгийский город с похожим названием.

Особенности адресов в ЕС

Руководства по массовому геокодированию для рынка США обходят стороной различия в форматах, на которых спотыкаются европейские наборы данных. Самые частые проблемы перечислены ниже.

Германия: порядок номера дома. Немецкие адреса записываются в формате {улица} {номер}: Berliner Straße 42. Многие CRM хранят адреса в формате {номер} {улица}, потому что создавались под британские и американские правила. Если немецкие адреса геокодируются с низкой уверенностью, попробуйте поменять местами номер и название улицы в строке запроса перед отправкой.

Франция: округа (arrondissements). Парижские адреса включают номер округа (с 1-го по 20-й) в почтовом индексе: от 75001 до 75020. Запросы без округа, только с Paris, геокодируются в центр города, а не в конкретный район. В результатах это выглядит как совпадение locality с низкой уверенностью, а не как совпадение по адресу.

Нидерланды: формат почтового индекса. Нидерландские индексы строго следуют шаблону DDDD LL (4 цифры, пробел, 2 заглавные буквы). Индексы без пробела (1012LG вместо 1012 LG) или со строчными буквами геокодируются корректно, но если формат индекса проверяется отдельно, приведите его к заглавным буквам с пробелом.

Бельгия: языковая неоднозначность. У некоторых бельгийских муниципалитетов разные французские и нидерландские названия (Liège/Luik, Gent/Gand). API понимает оба варианта, но непоследовательные названия в наборе данных (часть записей на французском, часть на нидерландском) могут давать разные уровни уверенности. Перед геокодированием приведите названия к одному языку для каждого региона.

Испания и Италия: варианты префиксов улиц. «Calle», «Carrer», «Via», «Viale» являются корректными префиксами типа улицы и в записях могут встречаться в сокращённом виде («C/»). Геокодер понимает распространённые сокращения, но для необычных сокращений из старых систем может понадобиться этап нормализации.

Увеличение пропускной способности с параллельными запросами

Последовательный скрипт надёжен и прост, но медленный. Для больших наборов данных замените последовательный цикл параллельным исполнителем:

from concurrent.futures import ThreadPoolExecutor, as_completed

def main_concurrent(max_workers=10):
    df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')
    # ... (та же подготовка, что и раньше) ...

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

    # ... (та же запись результатов, что и раньше) ...

С 10 параллельными потоками пропускную способность ограничивают задержка API и лимит вашего тарифа, а не паузы: при 50 запросах в секунду 10 000 адресов обрабатываются меньше чем за 4 минуты. В параллельной версии нет встроенного ограничения скорости, поэтому добавьте его или уменьшите max_workers, если тариф ограничивает число запросов в секунду. Перед увеличением max_workers проверьте лимит параллельных запросов в вашем тарифе MapAtlas.

Если в вашей организации проверка адресов проводится регулярно, например еженедельная очистка новых импортов в CRM или ночная проверка адресов доставки, посмотрите страницу решений для логистики и доставки: там описано, как интеграции MapAtlas встраиваются в рабочие процессы.

Если вы хотите не пускать плохие данные в систему с самого начала (ловить ошибки в источнике, а не чистить их пакетно), фронтенд-реализация описана в статье Address Autocomplete API: как одно поле повышает конверсию оформления заказа.

Работа с координатами в результатах

Проверенные результаты содержат geocoded_lat и geocoded_lng для каждого принятого адреса. Эти координаты открывают виды анализа, недоступные для сырых адресных строк:

  • Расчёт расстояний. Вычислите расстояние по прямой между складом и каждым адресом доставки, чтобы оценить тарифные зоны доставки.
  • Анализ зон охвата. Нанесите проверенные адреса клиентов на карту и посмотрите, где географически сосредоточен спрос.
  • Назначение зон доставки. Закрепите каждый адрес за зоной доставки, проверив, попадают ли его координаты в полигон зоны.
  • Поиск дубликатов. Две записи с одинаковыми координатами (в пределах нескольких метров), скорее всего, дублируют друг друга, даже если строки адреса отформатированы по-разному.

О согласованности NAP (Name/Address/Phone) и её влиянии на видимость в AI-поиске, что особенно важно для адресных баз локального бизнеса, рассказывает статья Согласованность NAP для AI-поиска: почему несовпадающие адреса лишают вас видимости в ChatGPT. В ней объясняется, почему адреса, проверенные геокодированием, служат правильной основой для разметки структурированных данных.

Итоги

Массовое геокодирование через MapAtlas Geocoding API даёт:

  • Проверенные адреса с оценками уверенности, чтобы понимать, каким записям доверять автоматически, а какие отправлять на проверку.
  • Структурированные компоненты (улица, номер дома, почтовый индекс, город, страна), нормализованные из любого входного формата.
  • Географические координаты для каждого принятого адреса, которые позволяют проводить пространственный анализ и строить маршруты.
  • Поддержку форматов адресов ЕС на уровне API, без необходимости обрабатывать их на этапе предварительной подготовки.

Python-скрипт обрабатывает 5 адресов в секунду в последовательном режиме и до 50 в секунду с параллельными потоками. На 10 000 адресов закладывайте от 4 до 50 минут в зависимости от параллельности и задержки. На выходе получается чистый CSV с тремя уровнями: accept, review, reject.


Получите бесплатный API-ключ MapAtlas, чтобы начать. Geocoding API поддерживает массовые запросы на всех тарифах, цены за запрос и лимиты бесплатного уровня указаны на странице тарифов.

Часто задаваемые вопросы

Сколько времени требуется для геокодирования 10 000 адресов с помощью MapAtlas API?

При скорости 5 запросов в секунду на 10 000 адресов уходит не меньше 33 минут одних пауз для соблюдения лимита плюс сетевая задержка каждого запроса, поэтому закладывайте 35-50 минут. С 10 параллельными потоками примерно при 50 запросах в секунду время сокращается до менее чем 5 минут, если лимит запросов вашего тарифа это позволяет. Python-скрипт в этом руководстве включает опции как последовательных, так и одновременных.

Какой порог оценки уверенности я должен использовать для валидации адреса?

Хорошо работает практическая трёхуровневая система: принимайте адреса с уверенностью выше 0,85, отмечайте для ручного просмотра между 0,60 и 0,85, отклоняйте ниже 0,60. Точные пороги зависят от того, насколько критична точность адреса для вашего сценария использования, операции логистики должны использовать более строгие пороги, чем маркетинговые кампании.

Обрабатывает ли MapAtlas Geocoding API форматы адресов, специфичные для ЕС?

Да. API правильно разбирает немецкие (номер дома после улицы), французские (номер дома перед улицей), голландские (4+2 почтовые коды) и другие соглашения формата адресов страны ЕС. Запросы на местном языке возвращают лучшие результаты, чем транслитерированные или англоязычные запросы.

Оказалось полезным? Поделитесь.

Об авторе

Brent van der Heiden

Автор

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.

Все статьи →
Вернуться в блог