«Причесать адрес» и «найти адрес в реестре» — две разные задачи, и путают их постоянно
Курьеру нужен разобранный адрес, бухгалтерии — код из государственного реестра. Это не одно и то же, и выбор не того инструмента стоит либо лишних денег, либо непринятых документов.
Задача звучала безобидно: привести в порядок 62 тысячи адресов в CRM перед переездом на
новую службу доставки. Разработчик взял поиск по государственному адресному реестру,
прогнал первые пять тысяч записей — и получил "count": 0 примерно у трети.
Строки при этом были совершенно живые: «мск, тверская 7 стр 2, 3 подъезд, домофон 45».
Курьер по такой найдёт дом за минуту. Реестр — не найдёт, потому что подъездов и домофонов
в реестре нет.
Через неделю та же тема пришла с другой стороны. Бухгалтерии понадобился ОКТМО по адресам
двенадцати филиалов — для отчёта, который сдаётся раз в квартал и который уже один раз
вернули. Разработчик, наученный прошлым разом, взял стандартизацию адреса. Получил
аккуратный разбор, "quality": 100, красивую нормализованную строку — и
"gar": null. Ни ОКТМО, ни индекса, ни кода КЛАДР. Их там нет и по замыслу
не должно быть.
Обе стороны совершили одну и ту же ошибку с разных концов. «Причесать адрес» и «найти адрес в реестре» — две разные операции, и совпадают они только тем, что обе про адреса.
Граница между ними проходит по одной линии. Стандартизация работает со строкой и ничего не знает о реальном мире: она разбирает текст, а существует ли такой дом — вопрос не к ней. ГАР работает с реестром и почти ничего не знает о том, как люди пишут адреса в свободном поле: этажи, ориентиры и «вход со двора» для него шум.
Где именно течёт
Ошибка выбора инструмента редко выглядит как ошибка. Код работает, ответы приходят, никаких исключений в логах. Просто результат оказывается не тем, за которым шли.
Прогнали базу через реестр и потеряли треть
- Что происходит
- В CRM адрес лежит одной строкой вместе с подъездом, этажом и комментарием для курьера. Поиск по реестру на такую строку возвращает пустой results и count = 0.
- Во что обходится
- Запрос выполнен, ответ получен, кредит списан. На 62 тысячах записей это двадцать тысяч оплаченных запросов, которые не могли сработать в принципе.
- Чем закрывается
- Сначала разбор строки: он выкидывает лишнее, раскладывает по уровням и собирает нормализованное представление. В реестр уходит уже адрес, а не заметка для курьера.
Разбор идеальный, кодов нет
- Что происходит
- Ответ стандартизации: quality 100, все уровни на месте, normalized выглядит как из учебника. Поля gar и geo — null.
- Во что обходится
- Квартальный отчёт возвращают повторно, бухгалтер ищет ОКТМО руками по сайту ФНС. Двенадцать филиалов — это полдня и три опечатки в кодах.
- Чем закрывается
- Коды живут в реестре, а не в парсере. Их отдаёт /api/gar/search в полях oktmo, okato, postalCode — либо /api/gar/object/{guid} в списке parameters с расшифровками типов.
quality 100 приняли за «адрес существует»
- Что происходит
- Строку «г Москва, ул Вымышленная, д.404» разбор принимает без единой претензии: структура правильная, ничего лишнего, коэффициент максимальный.
- Во что обходится
- Заказ уезжает на несуществующий дом, курьер возвращает его на склад, клиент получает возврат вместо покупки. Ошибка вскрывается на последней миле — самой дорогой.
- Чем закрывается
- Существование адреса подтверждает только реестр: /api/gar/search на такую строку вернёт count = 0. Разбор сюда добавить нечего, у него другая работа.
Сколько это стоит и в каком порядке звонить
Разница в деньгах возникает не из-за цены за запрос, а из-за числа запросов, которые вы сделаете зря.
Обозначим: N — записей в базе, d — доля строк, из которых разбор не смог выделить адрес (эти записи в реестр отправлять бессмысленно), q — доля тех, что разобрались, но с низким коэффициентом и уходят человеку на проверку. Наивный сценарий «всё в реестр» стоит N запросов к ГАР. Сценарий с предварительным разбором стоит N разборов плюс N × (1 − d − q) запросов к реестру. Разбор при этом дешевле: он выполняется локально, без обращений к внешним источникам, и у него есть бесплатный дневной лимит в веб-интерфейсе.
| Параметр | Пример (замерьте свой) | Откуда взять |
|---|---|---|
| N — записей в базе | 62 000 | SELECT COUNT из таблицы адресов |
| d — не разобрались вовсе | 0,06 | Доля ответов 400 на выборке в тысячу записей |
| q — разобрались плохо | 0,11 | Доля ответов с quality ниже вашего порога |
| Запросов к реестру: наивно | 62 000 | Каждая строка идёт в поиск как есть |
| Запросов к реестру: с разбором | 51 460 | N × (1 − 0,06 − 0,11) |
Десять с половиной тысяч запросов разницы — это на порядок больше, чем экономия на цене одного вызова. Но главное даже не в этом. Из 62 тысяч наивных запросов часть вернула бы мусорного кандидата вместо пустого ответа, и вот эти записи вы бы уже не отловили: в базе появился бы GUID чужого дома. Пустой ответ виден сразу, неправильный ответ — через месяц.
Какой сервис под какую задачу
Проще всего выбирать по одному вопросу: что должно оказаться в вашей базе после операции. Если строка — вам нужен разбор. Если идентификатор — вам нужен реестр.
| Стандартизация адреса | Адреса ГАР/ФИАС | |
|---|---|---|
| Маршрут | /api/addressstd |
/api/gar/search, /api/gar/suggest, /api/gar/object/{guid} |
| Что на входе | Одна строка как её написал человек — с опечатками, сокращениями, нижним регистром | Строка поиска: элементы адреса, можно через запятую |
| Что на выходе | components по уровням, normalized, quality, countryCode |
results с fullAddress, objectGuid, objectId, regionCode, postalCode, oktmo, okato |
| Сколько ответов | Ровно один разбор | Список кандидатов, count штук, выбор за вами |
| Терпит мусор в строке | Да, это его работа. Непонятное уходит в extra и message |
Плохо. Подъезд и домофон сбивают поиск |
| Подтверждает существование | Нет | Да — в пределах того, что есть в выгрузке ФНС |
| Типовое применение | Форма, импорт CSV, чистка legacy-базы, единый формат для перевозчика | Автодополнение в поле ввода, коды для отчётности, стабильный ключ для дедупликации |
Сценарий первый: только разбор
Клиент оставил адрес доставки, вам нужно передать его курьерской службе в её формате: город
отдельно, улица отдельно, дом с корпусом отдельно. Реестр здесь ничего не добавит — курьеру
не нужен GUID объекта, ему нужны поля. Один вызов /api/addressstd, разложили
components по колонкам, отдали.
Полезная деталь: параметр region — двузначный код региона. Если у вас интернет-
магазин с доставкой по одному городу и люди пишут «ленинский проспект 4» без города,
передавайте region=77, и верхний уровень восстановится сам.
Сценарий второй: только реестр
В форме заказа стоит поле с автодополнением. Человек начинает печатать, вы дёргаете
/api/gar/suggest (минимум два символа, по умолчанию семь подсказок, максимум
двадцать), он выбирает готовый вариант из списка. Разбирать нечего — строки в свободной
форме не возникло вообще. В базу вы кладёте objectId и objectGuid,
а не текст.
Это самый дешёвый способ решить проблему грязных адресов: не чистить их потом, а не дать им появиться. Но работает он только там, где есть живой пользователь и интерфейс.
Сценарий третий: оба, по очереди
Миграция legacy-базы, где адрес хранится строкой и нужен ОКТМО. Здесь два вызова, и порядок имеет значение.
- Разбор строки:
/api/addressstd. Получаемcomponentsиquality. - Отсев: всё ниже порога — в очередь на ручную проверку, дальше не идёт.
- Сборка поискового запроса из уровней от региона до дома. Квартиру, комнату и всё, что попало в
extra, в запрос не кладём. - Поиск:
/api/gar/search. Смотримcountи решаем, что делать с кандидатами.
| Что получили | Что делать |
|---|---|
400 от разбора |
Адрес из строки не выделен. В реестр не идём, запрос не оплачен. Запись — оператору |
quality ниже порога |
Разбор неуверенный. Поиск по нему даст правдоподобный, но чужой дом. Оператору |
count = 0 |
Реестр адрес не знает. Причины разные: снесённый дом, новостройка, ошибка в номере. Оператору |
count = 1 |
Единственный кандидат. Забираем objectGuid, postalCode, oktmo, okato |
count больше единицы |
Однофамильцы среди улиц — обычное дело. Если regionCode кандидата совпадает с ожидаемым и он один такой — берём его. Иначе оператору |
Код: два вызова и склейка между ними
Всё ниже работает с публичным демо-ключом ak_sandbox_demo_mockdata_v1 —
копируйте и запускайте, аккаунт не нужен. На демо-ключе оба сервиса отдают моки:
правдоподобные, но сгенерированные данные. Они детерминированы, один и тот же запрос всегда
даёт один и тот же ответ, поэтому на них удобно писать тесты. Настоящий разбор и настоящие
записи реестра приходят только с личным ключом.
# Строку кодируем через --data-urlencode: кириллица и пробелы в query иначе поедут не так
curl -G -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
--data-urlencode "address=мск ленинскй проспект д4 кв5" \
--data-urlencode "region=77" \
"https://atlorium.com/api/addressstd"
import requests
BASE = "https://atlorium.com"
HEADERS = {"Authorization": "Bearer ak_sandbox_demo_mockdata_v1"}
def parse_address(line: str, region: int | None = None) -> dict | None:
"""Разбор строки. None означает, что адрес из неё не выделен (400, без списания)."""
resp = requests.get(
f"{BASE}/api/addressstd",
params={"address": line, "region": region},
headers=HEADERS,
timeout=10,
)
if resp.status_code == 400:
return None
resp.raise_for_status()
return resp.json()
addr = parse_address("мск ленинскй проспект д4 кв5", region=77)
# components отсортированы сверху вниз: от страны к комнате.
# level — имя элемента перечисления, не число: Street, Building, Apartment...
levels = {c["level"]: c for c in addr["components"]}
street = levels.get("Street", {}).get("text")
house = levels.get("Building", {})
print(street, house.get("number"), house.get("buildNumber"))
using System.Net.Http.Json;
using System.Text.Json;
var http = new HttpClient { BaseAddress = new Uri("https://atlorium.com") };
http.DefaultRequestHeaders.Add("Authorization", "Bearer ak_sandbox_demo_mockdata_v1");
var line = Uri.EscapeDataString("мск ленинскй проспект д4 кв5");
var addr = await http.GetFromJsonAsync<JsonElement>($"/api/addressstd?address={line}®ion=77");
var quality = addr.GetProperty("quality").GetInt32();
var normalized = addr.GetProperty("normalized").GetString();
// Порог свой. Ниже — не в реестр, а человеку.
if (quality < 80)
Console.WriteLine($"На проверку: {normalized} (quality {quality})");
{
"input": "мск ленинскй проспект д4 кв5",
"normalized": "город Москва, проспект Ленинский, д.4, кв.5",
"quality": 100,
"countryCode": "RU",
"message": null,
"info": null,
"components": [
{ "level": "City", "levelTitle": "Город", "text": "город Москва",
"types": ["город"], "names": ["Москва"] },
{ "level": "Street", "levelTitle": "Улица", "text": "проспект Ленинский",
"types": ["проспект"], "names": ["Ленинский"] },
{ "level": "Building", "levelTitle": "Дом/здание", "text": "д.4", "number": "4" },
{ "level": "Apartment", "levelTitle": "Помещение/квартира", "text": "кв.5", "number": "5" }
],
"extra": null,
"milliseconds": 7,
"gar": null,
"geo": null
}
Тот же вызов на шести языках (Python, TypeScript, Go, Java, C#, PHP) — в репозитории address-standardization-api-client. Примеры запускаются сразу: ключ в них уже стоит демонстрационный.
Два поля из этого ответа заслуживают отдельного взгляда. message — это жалоба
разбора на конкретную неточность, и она же объясняет, почему коэффициент просел:
непонятный фрагмент, лишний хвост. info — сообщение, которое на качество не
влияет, чисто справочное. Логируйте оба: когда через месяц придёте разбираться, почему у
трёх тысяч записей quality около шестидесяти, ответ будет лежать именно там.
Поля gar и geo в контракте уже есть, но заполняются они только
в комбо-режимах, а те пока отвечают 501. Писать код в расчёте на то, что
addr["gar"]["oktmo"] когда-нибудь появится сам, не надо — сегодня это
гарантированный null.
# Автодополнение: минимум 2 символа, по умолчанию 7 подсказок, максимум 20
curl -G -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
--data-urlencode "query=Москва Тверская" \
--data-urlencode "limit=7" \
"https://atlorium.com/api/gar/suggest"
# Полный поиск: по умолчанию 10 результатов, максимум 100
curl -G -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
--data-urlencode "query=Москва ул Тверская д 7" \
--data-urlencode "limit=5" \
"https://atlorium.com/api/gar/search"
# Карточка объекта по GUID: иерархия и параметры с расшифровками
curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
"https://atlorium.com/api/gar/object/0c5b2444-70a0-4932-980c-b4dc0d3f02b5"
const HEADERS = { Authorization: "Bearer ak_sandbox_demo_mockdata_v1" };
const BASE = "https://atlorium.com";
// Для поля ввода нужен именно suggest: он легче search и не собирает параметры объекта.
async function suggest(query) {
if (query.trim().length < 2) return [];
const url = new URL(`${BASE}/api/gar/suggest`);
url.searchParams.set("query", query);
url.searchParams.set("limit", "7");
const r = await fetch(url, { headers: HEADERS });
const data = await r.json();
// items: text, objectId, objectGuid, objectType. Индекса и ОКТМО здесь НЕТ.
return data.items;
}
// Пользователь выбрал подсказку — за кодами идём отдельным запросом.
async function details(objectGuid) {
const r = await fetch(`${BASE}/api/gar/object/${objectGuid}`, { headers: HEADERS });
const obj = await r.json();
const byName = Object.fromEntries(
obj.parameters.map((p) => [p.typeName, p.value])
);
return {
fullAddress: obj.fullAddress,
postalCode: byName["Почтовый индекс"],
oktmo: byName["ОКТМО"],
kladr: byName["Код КЛАДР"],
};
}
// GET /api/gar/search?query=Москва ул Тверская д 7&limit=5
{
"query": "Москва ул Тверская д 7",
"results": [
{
"fullAddress": "г Москва, ул Тверская, д. 7",
"objectType": "дом",
"objectGuid": "0c5b2444-70a0-4932-980c-b4dc0d3f02b5",
"objectId": 1405113,
"regionCode": "77",
"postalCode": "125009",
"oktmo": "45382000",
"okato": "45286585000"
}
],
"count": 1,
"elapsedMs": 18
}
// GET /api/gar/object/0c5b2444-70a0-4932-980c-b4dc0d3f02b5
{
"objectId": 1405113,
"objectGuid": "0c5b2444-70a0-4932-980c-b4dc0d3f02b5",
"fullAddress": "г Москва, ул Тверская, д. 7",
"objectType": "дом",
"regionCode": "77",
"hierarchy": [
{ "objectId": 1405113, "displayName": "Москва" },
{ "objectId": 1408743, "displayName": "ул Тверская" },
{ "objectId": 1405113, "displayName": "д. 7" }
],
"parameters": [
{ "typeId": 5, "typeName": "Почтовый индекс", "value": "125009" },
{ "typeId": 6, "typeName": "ОКТМО", "value": "45382000" },
{ "typeId": 7, "typeName": "ОКАТО", "value": "45286585000" },
{ "typeId": 8, "typeName": "Код ИФНС", "value": "7710" },
{ "typeId": 10, "typeName": "Код КЛАДР", "value": "77000000000072200" }
],
"elapsedMs": 12
}
Тот же вызов на шести языках (Python, TypeScript, Go, Java, C#, PHP) — в репозитории gar-fias-address-api-client. Примеры запускаются сразу: ключ в них уже стоит демонстрационный.
Обратите внимание на разницу между двумя эндпоинтами. search уже отдаёт индекс,
ОКТМО и ОКАТО прямо в каждом результате — для миграции базы этого достаточно, лишний запрос
не нужен. А вот suggest нарочно облегчён: только text,
objectId, objectGuid и objectType, потому что дёргается
он на каждое нажатие клавиши. Коды по выбранной подсказке добираются отдельным вызовом
object/{guid}, и в parameters их приходит больше: там же код ИФНС и
КЛАДР, которых в результатах поиска нет.
Ещё в карточке объекта есть hierarchy — путь от региона до самого объекта.
Если вам нужен не адрес целиком, а его верхние уровни (например, регион и город отдельными
полями и обязательно в том написании, в каком они в реестре), берите оттуда, а не разбирайте
fullAddress регулярками.
# Уровни, которые имеет смысл класть в поисковый запрос к реестру.
# Квартиры и комнаты в ГАР есть, но искать по ним строкой — лишний шум.
SEARCH_LEVELS = (
"Country", "RegionArea", "RegionCity", "District", "Settlement",
"City", "CityDistrict", "Locality", "Territory", "Street", "Building",
)
QUALITY_THRESHOLD = 80 # свой, подобранный на своей выборке
def to_search_query(addr: dict) -> str:
"""Из разобранных уровней собираем строку для поиска в реестре."""
parts = [
c["text"] for c in addr["components"]
if c["level"] in SEARCH_LEVELS
]
return ", ".join(parts)
def gar_search(query: str, limit: int = 5) -> dict:
resp = requests.get(
f"{BASE}/api/gar/search",
params={"query": query, "limit": limit},
headers=HEADERS,
timeout=15,
)
resp.raise_for_status()
return resp.json()
def enrich(line: str, region: int | None = None) -> dict:
addr = parse_address(line, region)
if addr is None:
return {"state": "unparsed", "input": line}
if addr["quality"] < QUALITY_THRESHOLD:
return {"state": "low_quality", "normalized": addr["normalized"],
"quality": addr["quality"], "message": addr["message"]}
found = gar_search(to_search_query(addr))
if found["count"] == 0:
return {"state": "not_in_registry", "normalized": addr["normalized"]}
if found["count"] > 1:
return {"state": "ambiguous", "candidates": found["results"]}
hit = found["results"][0]
return {
"state": "ok",
"normalized": addr["normalized"],
"objectGuid": hit["objectGuid"],
"objectId": hit["objectId"],
"postalCode": hit["postalCode"],
"oktmo": hit["oktmo"],
"okato": hit["okato"],
}
// Пять исходов вместо булева «получилось / не получилось».
// Три из них — работа для человека, и это нормально: полностью
// автоматической миграции адресной базы не бывает.
public enum EnrichState
{
Ok, // разобрано и найдено в реестре ровно одно
Unparsed, // 400 от разбора: адреса в строке нет, списания не было
LowQuality, // quality ниже порога — в реестр не пускаем
NotInRegistry, // count = 0
Ambiguous, // count больше одного, выбор за человеком
}
public sealed record EnrichResult(
EnrichState State,
string? Normalized,
int? Quality,
string? ObjectGuid,
long? ObjectId,
string? PostalCode,
string? Oktmo,
string? Okato);
Тот же вызов на шести языках (Python, TypeScript, Go, Java, C#, PHP) — в репозитории address-standardization-api-client. Примеры запускаются сразу: ключ в них уже стоит демонстрационный.
Пять исходов вместо двух — не усложнение, а признание факта. Записи, которые нельзя обработать автоматически, есть всегда, и разница между хорошей миграцией и плохой в том, попадают ли они в отдельную очередь или молча уезжают в базу с чужим GUID.
Чего эти два сервиса не умеют
Раздел, из-за которого статью стоит дочитать до конца.
Разбор не подтверждает существование адреса. Он работает с текстом и оценивает
качество распознавания текста. Дом с номером 404 на существующей улице разбирается на
quality 100 и попадает в базу как безупречный. Единственное, что отвечает на
вопрос «а есть ли такой дом», — поиск в реестре.
Комбо-режима пока нет. Параметры bind и geocode описаны в
контракте, но возвращают 501. Ни привязки к реестру одним вызовом, ни координат
сегодня получить нельзя, и поля gar и geo в ответе всегда пустые.
Связку из двух запросов вы собираете сами — код выше и есть эта связка.
Мы не гарантируем полноту реестра и не даём его данным юридической силы. Справочник
строится из официальной выгрузки ФНС и отдаёт ровно то, что в ней есть. Новостройка, которую
в реестр ещё не внесли, вернёт count: 0 — и это будет правдой про реестр, а не
про дом. Обратная ситуация тоже встречается: снесённое здание может оставаться в выгрузке.
Поиск в реестре не разбирает свободный текст. Он ищет по адресным объектам: регион,
населённый пункт, улица, дом, помещение. Этажи, подъезды, коды домофона, ориентиры вроде
«напротив школы», номера абонентских ящиков — всё это для поиска шум, и именно из-за него
прямой прогон строк из CRM даёт пустые ответы. Разбор такие фрагменты как раз отделяет: часть
уходит в extra, часть отражается в message.
Запрос карточки объекта по несуществующему GUID тарифицируется. Ответ будет
404, но кредит спишется: обращение к справочнику уже выполнено, ресурсы
потрачены. Это осознанное решение, и лучше узнать о нём из статьи, чем из счёта. У поиска
такой проблемы нет: пустой результат — это 200 с count: 0, обычный
оплаченный запрос.
В песочнице ветвлений почти нет. Демо-ключ отдаёт разбор с quality 100 и
gar: null, а поиск — пять сгенерированных домов на случайных улицах. Проверить на
моках, как ваш код поведёт себя при count: 0 или при низком качестве разбора,
не получится: эти ветки надо покрывать своими тестами с подставленным ответом. На моках
удобно другое — убедиться, что вы правильно поняли форму ответа и разбираете нужные поля.
Частые вопросы
Частые вопросы
Чем нормализация адреса отличается от поиска в ГАР/ФИАС?
Нормализация работает со строкой: разбирает текст на уровни от региона до квартиры, приводит к единому виду и оценивает качество разбора числом от 0 до 100. Существование адреса она не проверяет. Поиск в ГАР ищет запись в государственном адресном реестре и отдаёт идентификаторы объекта: GUID, числовой ObjectId, почтовый индекс, ОКТМО и ОКАТО. Первое чинит формат, второе подтверждает реальность.
Можно ли одним запросом получить и разобранный адрес, и код ГАР?
Пока нет. У сервиса стандартизации есть параметр bind для привязки разобранного адреса к реестру, но он в разработке и возвращает код 501 — при этом деньги за него не списываются. Сегодня это два последовательных вызова: сначала /api/addressstd, потом /api/gar/search по собранной из уровней строке.
Что означает коэффициент качества и какой порог выбрать?
Коэффициент 0–100 показывает, насколько уверенно разобрана строка: 100 — весь текст распознан и лишних фрагментов не осталось, от 90 считается нормой, ниже 80 к результату стоит относиться осторожно. Порог для своего кода подбирают на выборке из собственной базы: у адресов интернет-магазина и у адресов юрлиц он обычно разный.
Как получить ОКТМО и почтовый индекс по адресу?
Через поиск в реестре: GET /api/gar/search возвращает в каждом результате поля postalCode, oktmo и okato. Если нужен ещё код КЛАДР или код ИФНС, запросите карточку объекта по GUID через /api/gar/object/{guid} — там они лежат в списке parameters вместе с расшифровкой типа. Разбор строки эти коды не выдаёт: их нет в самой строке.
Что использовать в поле ввода адреса на сайте — suggest или search?
Suggest. Он облегчённый и отдаёт только текст подсказки, ObjectId, ObjectGuid и тип объекта, поэтому его не жалко дёргать на каждое нажатие клавиши; минимум два символа, по умолчанию семь подсказок. Когда пользователь выбрал вариант, за индексом и кодами идите отдельным запросом по GUID.
Сколько запросов нужно на миграцию базы адресов?
По одному разбору на каждую запись плюс по одному поиску в реестре на те записи, которые разобрались с приемлемым качеством. Строки, из которых адрес выделить не удалось, возвращают 400 и не тарифицируются вовсе, так что они в счёт не попадут. Прогонять исходные строки сразу через реестр дороже и хуже: часть вернёт пустой ответ, а часть — правдоподобного, но чужого кандидата.
Сервисы из этой статьи
Разбор адресной строки в структуру: уровни (регион/город/улица/дом/квартира), нормализация, коэффициент качества
Попробовать прямо сейчас — без регистрации
Демо-ключ ak_sandbox_demo_mockdata_v1 — публичный и общий для всех.
С ним API отвечает моками: данные правдоподобные, но сгенерированные,
и они не меняются от запроса к запросу — на них удобно писать тесты.
Настоящие данные приходят с личным ключом.
curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
https://atlorium.com/openapi/addressstd_ru.json