Адрес есть, координат нет: как геокодировать накопленную базу и не поверить в красивый мусор
Геокодер почти всегда что-нибудь возвращает — и в этом главная ловушка. Разбираем, как отличить координату дома от центра улицы, во что обходится пакетный прогон и где считать результат непригодным.
В понедельник аналитик выгрузил из CRM 38 тысяч адресов доставки и прогнал их через геокодер. Ответ пришёл почти на всё — пустых оказалось меньше двух процентов. Карта покрылась точками, зоны доставки пересчитались, новый тариф на выезд за город ушёл в прайс к вечеру среды.
Через месяц логист заметил, что в одном подмосковном посёлке все двести с лишним адресов
стоят в одной точке — на въезде. Геокодер не знал в этом посёлке ни одного дома и вернул
то, что знал: центр населённого пункта. Формально он не соврал. В ответе у каждой такой
точки был уровень точности, и он честно говорил «это центр посёлка, а не ваш дом».
Просто разбор ответа этого поля не читал: код брал latitude и
longitude и клал их в базу.
Так портятся геоданные чаще всего. Не оттого, что геокодер ошибся, а оттого, что он ответил на вопрос, которого ему не задавали, — и ответ приняли за другой.
Дальше — про то, как отличить координату дома от центра улицы, во сколько обходится прогон накопленной базы, в каком порядке звать сервисы и где результат честнее считать непригодным.
Где именно течёт
Геокодер устроен так, что почти всегда возвращает хоть что-нибудь: город из запроса он узнаёт даже тогда, когда не узнал улицу. Это удобно на демонстрации и разрушительно на реальной выгрузке, где часть адресов заведомо неполна.
Двести адресов в одной точке
- Что происходит
- В посёлке нет ни одного дома с координатой. Геокодер отдаёт центр населённого пункта, координаты у всех записей совпадают до шестого знака.
- Во что обходится
- Зоны доставки посчитаны по неверным точкам, курьер получает маршрут к въезду, клиент — звонок «выйдите к шлагбауму». Ошибка вскрывается на последней миле, самой дорогой.
- Чем закрывается
- Уровень точности приходит в каждом варианте, а результаты грубее центра улицы наш сервис не отдаёт совсем: вместо красивой ложной точки будет 404, и запрос не спишется.
«13к2» стал домом 13
- Что происходит
- Корпус в реестре не заведён отдельной записью. Совпадение неполное, координата берётся от дома 13 — соседнего здания того же владельца.
- Во что обходится
- Промах на 80–150 метров в плотной застройке. Для карты это незаметно, для расчёта времени доставки и для привязки объекта к зданию — уже нет.
- Чем закрывается
- Такой ответ помечен уровнем range. Если приблизительная координата хуже её отсутствия, включается режим exactOnly: он оставляет только буквальные попадания, а остальное превращает в бесплатное «не найдено».
В геокодер уехала строка из формы целиком
- Что происходит
- «мск, тверская 7 стр 2, 3 подъезд, домофон 45, после 18» — вместе с подъездом, домофоном и пожеланием по времени.
- Во что обходится
- Часть строк не находится вовсе, часть находится не тем: лишние фрагменты сбивают разбор запроса. На десятках тысяч записей это заметная доля ручной работы.
- Чем закрывается
- Строку сначала разбирает стандартизация адреса, и в геокодер уходит адрес, а не заметка для курьера. Порядок операций разобран ниже.
Сколько стоит прогнать базу
Считать надо не запросы, а результаты. Правило тарификации здесь одно: списывается только найденный адрес, прошедший порог качества. Ответ «не найдено», ответ «найдено, но грубее центра улицы», некорректный ввод и недоступность сервиса не оплачиваются.
В пакетном режиме то же правило работает построчно. Пакет из тысячи адресов, из которых
нашлось семьсот, стоит семьсот единиц, а не тысячу. Сколько именно списано, приходит прямо
в ответе полем billedUnits — сверять с личным кабинетом не нужно.
| Что считаем | Пример | Откуда взять свою цифру |
|---|---|---|
| N — записей в базе | 38 000 | Количество строк в выгрузке адресов |
| Запросов к API | 38 | N ÷ 1000: пакет принимает до 1000 строк за вызов |
| Оплаченных единиц | N × доля найденных | Поле found в ответе на первой же тысяче |
| Из них пригодных «до дома» | доля exact и range |
Разложить свои результаты по precision |
| Ручная очередь | ненайденные + street |
Всё, что не прошло ваш порог точности |
Замерять долю найденных на всей базе не нужно и не стоит: прогоните первую тысячу и
посмотрите на found и на распределение по precision. Выгрузка
обычно однородна, и первая тысяча предсказывает остальные тридцать семь с точностью,
которой достаточно для планирования.
Порядок операций: разбор, реестр, координаты
Три сервиса решают три разные задачи, и путать их дорого. Стандартизация адреса работает со строкой: разбирает текст на уровни, выкидывает подъезды и комментарии, приводит запись к единому виду. ГАР/ФИАС работает с государственным реестром: подтверждает, что адрес существует, и выдаёт коды — почтовый индекс, ОКТМО, ОКАТО, КЛАДР. Геокодинг работает с картой: отвечает, где это находится.
| Шаг | Сервис | Зачем нужен именно здесь |
|---|---|---|
| 1 | Стандартизация адреса | Из строки формы делает адрес: уровни отдельно, мусор отдельно. Записи, из которых адрес выделить не удалось, дальше не идут — и не тратят платные запросы. |
| 2 | ГАР/ФИАС | Нужен, если вам требуются коды и подтверждение существования адреса. Для чистой задачи «нанести на карту» шаг пропускается. |
| 3 | Прямой геокодинг | Даёт координату и уровень её точности. Здесь же выбирается порог: что кладём в базу автоматически, а что уходит человеку. |
Если адрес уже хранится у вас разложенным по полям, третий шаг делается не свободной
строкой, а структурированным запросом: address, locality,
region, postalCode. Разбор свободной строки — самый ненадёжный
этап геокодирования, потому что геокодеру приходится угадывать, где город, а где улица.
Готовые поля этот этап снимают. Особенно заметно на названиях, которые встречаются и как
улица, и как населённый пункт.
Код: пакетный прогон и разбор ответа
Все примеры работают на публичном демо-ключе ak_sandbox_demo_mockdata_v1. Он
отдаёт моки — согласованный между собой набор вокруг дома в Казани. Данные вымышленные, но
форма ответа настоящая, и на ней можно написать разбор до регистрации. В пакетном демо
каждая третья строка намеренно не находится: именно этот случай в реальной выгрузке
обрабатывается хуже всего.
# Пакет: до 1000 строк за вызов, ответ строго в порядке запроса
curl -X POST \
-H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
-H "Content-Type: application/json" \
-d '{"queries":["Казань, улица Баумана, 13","Москва, Тверская 1","нераспознаваемая строка"],"sizePerQuery":1}' \
"https://atlorium.com/api/geocodeforward/batch"
# Одиночный адрес строкой
curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
"https://atlorium.com/api/geocodeforward?text=Казань,%20улица%20Баумана,%2013&size=5"
import requests
BASE = "https://atlorium.com"
HEADERS = {"Authorization": "Bearer ak_sandbox_demo_mockdata_v1"}
# Уровни точности упорядочены от лучшего к худшему.
PRECISION_RANK = {"exact": 0, "range": 1, "street": 2, "locality": 3, "area": 4, "none": 5}
# Свой порог. Для доставки до подъезда разумно "range", для аналитики по районам хватит "street".
THRESHOLD = "range"
def geocode_batch(queries: list[str]) -> dict:
"""Пакет до 1000 строк. Больше — отклоняется целиком с кодом 400."""
resp = requests.post(
f"{BASE}/api/geocodeforward/batch",
json={"queries": queries, "sizePerQuery": 1},
headers=HEADERS,
timeout=60,
)
resp.raise_for_status()
return resp.json()
def classify(item: dict) -> tuple[str, dict | None]:
"""Три исхода вместо двух: взяли, отправили человеку, не нашли."""
if not item["matches"]:
return "not_found", None
best = item["matches"][0]
if PRECISION_RANK[best["precision"]] <= PRECISION_RANK[THRESHOLD]:
return "ok", best
return "review", best
chunk = ["Казань, улица Баумана, 13", "Москва, Тверская 1", "нераспознаваемая строка"]
data = geocode_batch(chunk)
# billedUnits меньше requested ровно на число ненайденных строк.
print(data["requested"], data["found"], data["billedUnits"])
for item in data["items"]:
state, match = classify(item)
if state == "ok":
point = match["point"]
save(item["query"], point["latitude"], point["longitude"], match["placeId"])
else:
to_manual_queue(item["query"], state)
// Порог точности — часть бизнес-логики, а не деталь интеграции.
// Уровни упорядочены, поэтому проверка пишется одним сравнением.
public enum GeocodePrecision { Exact, Range, Street, Locality, Area, None }
public sealed record GeocodeDecision(
string Query,
double? Latitude,
double? Longitude,
string? PlaceId,
GeocodePrecision Precision,
bool NeedsHuman);
private static GeocodeDecision Decide(BatchItem item, GeocodePrecision threshold)
{
var best = item.Matches.FirstOrDefault();
if (best is null)
return new GeocodeDecision(item.Query, null, null, null, GeocodePrecision.None, true);
return new GeocodeDecision(
item.Query,
best.Point.Latitude,
best.Point.Longitude,
best.PlaceId,
best.Precision,
NeedsHuman: best.Precision > threshold);
}
// POST /api/geocodeforward/batch
{
"items": [
{
"query": "Казань, улица Баумана, 13",
"matches": [
{
"placeId": "11111111-2222-3333-4444-555555555555",
"label": "Респ Татарстан, г Казань, ул Баумана, 13",
"name": "13",
"houseNumber": "13",
"street": "ул Баумана",
"postalCode": "420111",
"point": { "latitude": 55.788226, "longitude": 49.121984 },
"layer": "address",
"precision": "exact",
"admin": {
"region": "Респ Татарстан",
"county": "г Казань",
"locality": "г Казань",
"street": "ул Баумана"
},
"parentPlaceId": "66666666-7777-8888-9999-000000000000"
}
]
},
{ "query": "Москва, Тверская 1", "matches": [ /* ... */ ] },
{ "query": "нераспознаваемая строка", "matches": [] }
],
"requested": 3,
"found": 2,
"billedUnits": 2,
"attribution": "Адресные данные: ГАР/ФИАС (ФНС России), открытые данные. Координаты: © участники OpenStreetMap (ODbL)."
}
Тот же вызов на шести языках (Python, TypeScript, Go, Java, C#, PHP) — в репозитории geocoding-api-client. Примеры запускаются сразу: ключ в них уже стоит демонстрационный.
Обратите внимание на две вещи в ответе. Первая: у каждой присланной строки есть свой
элемент, даже у ненайденной, и рядом с результатом лежит исходная строка — склейка со своей
таблицей по позиции остаётся возможной, но перестаёт быть единственным способом. Вторая:
billedUnits меньше requested ровно на число ненайденных.
Поле attribution приходит в каждом успешном ответе, и его нужно показывать
там, где вы показываете полученные данные. Это не вежливость: исходные наборы открытые, но
их лицензия требует указания авторства при любом использовании.
Обратная задача: точка есть, адреса нет
Трек курьера, отметка на карте, координаты из фотографии, геопозиция из мобильного приложения — здесь известна точка, а нужен адрес. Это отдельный сервис с отдельным тарифом, и устроен он иначе: базовый ответ стоит одну единицу работы, а каждое обогащение добавляет ещё одну.
| Параметр | Что добавляет | Когда включать |
|---|---|---|
| базовый запрос | address — ближайший адрес с расстоянием и уровнем точности |
Всегда: это и есть ответ на вопрос «какой здесь адрес» |
includeNeighbors |
neighbors — следующие по близости дома |
Точка стоит между двумя зданиями или у торца длинного дома |
includeAdmin |
adminAreas — образования, в границы которых попадает точка |
Нужен район и регион самой точки, а не ближайшего дома |
includeNearby |
nearby — ориентиры вокруг |
Нужно описать место человеку: остановка, магазин, заправка рядом |
Списывается при этом факт, а не план. Резервируется худший случай — «все включённые
обогащения сработали», — а платите вы за то, что действительно вернулось. Попросили адрес
и ориентиры вокруг, ориентиров в радиусе не нашлось: спишется одна единица, вторая
вернётся на баланс. Число приходит в ответе полем billedUnits.
# Базовый ответ — одна единица работы
curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
"https://atlorium.com/api/geocodereverse?lat=55.788226&lon=49.121984"
# С обогащениями: соседние дома и принадлежность точки по границам
curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
"https://atlorium.com/api/geocodereverse?lat=55.788226&lon=49.121984&includeNeighbors=true&includeAdmin=true"
# Только ориентиры вокруг точки, радиус в километрах
curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
"https://atlorium.com/api/geocodereverse/nearby?lat=55.788226&lon=49.121984&radiusKm=1&size=10"
def reverse(lat: float, lon: float, *, neighbors=False, admin=False, nearby=False) -> dict | None:
resp = requests.get(
f"{BASE}/api/geocodereverse",
params={
"lat": lat,
"lon": lon,
"includeNeighbors": str(neighbors).lower(),
"includeAdmin": str(admin).lower(),
"includeNearby": str(nearby).lower(),
},
headers=HEADERS,
timeout=15,
)
# 404 — адреса у точки нет либо ближайший объект дальше пяти километров.
# Это нормальный исход посреди поля, и он не тарифицируется.
if resp.status_code == 404:
return None
resp.raise_for_status()
data = resp.json()
# Две административные иерархии отвечают на РАЗНЫЕ вопросы:
# data["address"]["admin"] — где ЧИСЛИТСЯ найденный дом (выписка из реестра);
# data["adminAreas"] — внутри каких границ ЛЕЖИТ сама точка (расчёт по полигонам).
# Для дома в городе они совпадают, для точки в поле — расходятся. Склеивать нельзя.
return data
// GET /api/geocodereverse?lat=55.788226&lon=49.121984&includeAdmin=true
{
"query": { "latitude": 55.788226, "longitude": 49.121984 },
"address": {
"placeId": "11111111-2222-3333-4444-555555555555",
"label": "Респ Татарстан, г Казань, ул Баумана, 13",
"houseNumber": "13",
"street": "ул Баумана",
"postalCode": "420111",
"point": { "latitude": 55.788226, "longitude": 49.121984 },
"layer": "address",
"precision": "exact",
"distanceKm": 0.0,
"admin": { "region": "Респ Татарстан", "county": "г Казань", "locality": "г Казань", "street": "ул Баумана" }
},
"adminAreas": [
{ "name": "Татарстан", "kind": "респ", "level": 1, "certainty": "boundary" },
{ "name": "Казань", "kind": "г", "level": 5, "certainty": "boundary" }
],
"billedUnits": 2,
"attribution": "Адресные данные: ГАР/ФИАС (ФНС России), открытые данные. Координаты: © участники OpenStreetMap (ODbL)."
}
Тот же вызов на шести языках (Python, TypeScript, Go, Java, C#, PHP) — в репозитории reverse-geocoding-api-client. Примеры запускаются сразу: ключ в них уже стоит демонстрационный.
Расхождение двух иерархий — не ошибка, а содержательный ответ. Дом на окраине может числиться за деревней одного района, а точка в двухстах метрах от него — лежать в границах другого. Принадлежность точки считается попаданием в полигон именно ради этого случая: иначе точка в чистом поле унаследовала бы район ближайшей деревни за двадцать километров.
Чего это не умеет
Раздел, ради которого статью стоит дочитать.
Координата есть почти у всего, но нужного качества — не у всего. Цифры мы измерили на полной заливке реестра, а не оценили. У 16 % объектов координата своя, взята с карты; ещё у 17,5 % она вычислена между соседними домами. Это и есть попадание в здание — треть реестра. Ещё у 21 % известна улица, и отдаётся её центр. У остальных точнее населённого пункта ничего нет — такой ответ сервис не отдаёт вовсе, он превращается в бесплатное «адрес не найден».
Итог для планирования: из реестра целиком поиск вернёт результат примерно по половине объектов, и у двух третей найденного координата будет уровня здания. Причина не в недоработке: в государственном реестре около 60 миллионов записей, в открытой карте — около 11 миллионов адресных точек, и четверть реестровых объектов — это земельные участки, которым в карте соответствия нет в принципе. Ваша выборка почти наверняка окажется лучше этих цифр: жилые дома в городах размечены на карте плотнее, чем участки в полях, а геокодируют обычно адреса клиентов, а не реестр целиком. Прогоните свою тысячу строк на демо-ключе и считайте по ней.
Только Россия. Данные загружены по территории Российской Федерации. Зарубежный адрес вернёт «не найдено» — бесплатно, но и бесполезно. Международную логистику на этом сервисе планировать нельзя.
Это не адресный справочник. Сервис отвечает, где объект находится. Официальное написание адреса, коды и подтверждение существования записи — работа ГАР/ФИАС и стандартизации адреса. Мы не пытаемся их заменить, и связка из двух вызовов остаётся нормальным способом решить задачу целиком.
Точки предпочтения нет. Сказать «ищи рядом вот с этой координатой» нельзя: запрос
принимает адрес и количество вариантов, а не центр поиска. Если у вас есть подсказка о
регионе — передавайте её полем region структурированного запроса, это работает
лучше и предсказуемее.
Границы есть не у всего и упрощены. Геометрию в GeoJSON имеют административные образования — регион, район, город. У дома и улицы её нет: запрос не станет ошибкой, поле останется пустым, и дополнительная единица работы не спишется. Сама геометрия упрощена с допуском около ста метров, иначе ответ по субъекту весил бы десятки мегабайт. Точки у самой границы могут попасть не на ту сторону — для точного ответа о принадлежности берите обратный геокодинг, он считает по неупрощённой границе.
Ни квартир, ни подъездов. Самый подробный уровень — здание с номером дома. Ни помещений, ни жильцов в данных нет и быть не может: сервис работает с картой и реестром, а не с персональными данными.
Демо-ключ отдаёт один и тот же набор. В геокодинге это сделано намеренно: случайные координаты клиент ставит на карту и следующий час ищет несуществующую ошибку в своём коде. На моках удобно проверить форму ответа и разбор полей, а вот ветки «не найдено» и «низкая точность» на реальных данных придётся проверять своим ключом — кроме пакетного режима, где каждая третья строка не находится специально.
Частые вопросы
Частые вопросы
Что такое геокодирование адреса?
Это превращение адреса в координаты: на входе текст вроде «Казань, улица Баумана, 13», на выходе широта и долгота в системе WGS 84 — той же, что у GPS и у карт. Обратная задача, из координат в адрес, называется обратным геокодированием и решается отдельным сервисом.
Чем precision exact отличается от range?
Exact означает, что дом есть в данных и координата принадлежит именно ему. Range — одно из двух: либо положение дома вычислено между координатами соседей на улице пропорционально номеру, либо совпадение оказалось неполным, например спрашивали «13к2», а в реестре дом заведён одной записью «13». Попасть в нужный квартал такой результат позволяет, указать конкретное здание — нет.
Как геокодировать сразу всю базу адресов?
Пакетным режимом: POST на /api/geocodeforward/batch принимает до 1000 строк за вызов, выгрузка большего размера разбивается на части. Ответ приходит строго в порядке запроса, по одному элементу на каждую присланную строку, и каждый элемент несёт исходную строку рядом с результатом. Ненайденная строка пакет не обрывает и не оплачивается.
Сколько списывается, если адрес не найден?
Ничего. Тарификация идёт по результату: ответы «не найдено» и «найдено грубее центра улицы», некорректный ввод и недоступность сервиса не оплачиваются. В пакете счёт ведётся построчно, и фактическое число списанных единиц приходит в ответе полем billedUnits.
Можно ли получить координаты по почтовому индексу?
Да, но ответом будут не дома, а адресообразующие объекты: населённый пункт и улицы, которым принадлежит индекс. Домов на один индекс десятки тысяч, и такой список не отвечает на заданный вопрос. Режим удобен для автозаполнения формы и для проверки, что индекс в присланных данных не противоречит городу.
Чем геокодинг отличается от поиска в ГАР/ФИАС?
Геокодинг отвечает на вопрос «где это находится» и даёт координату с уровнем её точности. Поиск в государственном адресном реестре отвечает на вопрос «существует ли такой адрес» и выдаёт идентификаторы и коды — индекс, ОКТМО, ОКАТО, КЛАДР. Для миграции базы обычно нужны оба, и звать их стоит в таком порядке: сначала разбор строки, потом реестр, потом координаты.
Сервисы из этой статьи
Координаты по адресной строке: широта, долгота, разобранная структура и уровень точности. Оплата только за найденное
Адрес по координатам: ближайший дом и расстояние до него. По желанию — соседние дома, административная принадлежность точки и ориентиры вокруг, каждое дополнение отдельной единицей
Разбор адресной строки в структуру: уровни (регион/город/улица/дом/квартира), нормализация, коэффициент качества
Попробовать прямо сейчас — без регистрации
Демо-ключ ak_sandbox_demo_mockdata_v1 — публичный и общий для всех.
С ним API отвечает моками: данные правдоподобные, но сгенерированные,
и они не меняются от запроса к запросу — на них удобно писать тесты.
Настоящие данные приходят с личным ключом.
curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
https://atlorium.com/openapi/geo_ru.json