Ariadna. Зачем нужен еще один геокодер для ОСМ?
Всем привет!
Совсем недавно я закончил делать геокодер для своих целей Ariadna
Под катом рассказ о том, зачем я его делал и что он умеет.
В статье не будет ни строчки кода на Go. Зато будет полное описание работы геокодера и проблем, с которыми я встречался. А код можете посмотреть на гитхабе.
Предыстория
Я работаю в одной из бишкекских служб такси, что в Кыргызстане. Решили мы пооптимизировать нахождение координат по адресу для более оптимального распределения заказов.
Чего у нас нет:
- Полной карты Яндекса, Гугла или 2Гис
- Доверия к GPS данным
Что у нас есть:
- Очень разношерстные данные на входе
- Openstreetmap
- Своя накопленная база адресов с координатами
Что пользователи могут вводить?
Пользователи могут вводить адреса в разных форматах:
- Улица дом
- Перекресток
- Название заведения
- Название точки
- микрорайон дом
- микрорайон улица дом
И таких вариантов очень много, например
Киевская 28
Киевская Советская
5–42
5 микрорайон советская 42
ЦУМ
кафе у Ашота
шлагбаум
И так далее
Постановка задачи
Сделать геокодер, который бы умел принимать любые данные на вход и отдавать им координаты
Язык поиска — только русский.
Как докатился до такой жизни
Перед тем, как делать свой велосипед я решил поресерчить решения, которые уже есть.
Были:
Чем не устраивал номинатим, который у нас был:
- Сложен сам по себе
- Сделан на php+с (Это не потому что пхп плохой, а потому что только для этого инструмента у нас стоит апач и пхп)
- Сложная логика в хранимых процедурах Postgresql
Чем понравился Pelias:
- Может работать с многими источниками геоданных
- Поиск организован на ElasticSearch
В итоге решил отказаться от всех трех геокодеров и сделать свой инструмент по нескольким причинам:
- Я хочу разобраться с данными в OSM и импортить только нужные для поиска
- Я могу обрабатывать геоданные перед занесением в индекс
- Мне не нравиться жаваскрипт и node.js, отсюда и не желание делать поиск на основе пелиаса
Проектирование
Был заложен следующий алгоритм:
- Сначала получаем геометрию по крупным населенным пунктам (города, столицы, деревни, жилые массивы)
- Выгружаем все возможные адреса и соотносим их к нужному жилому массиву, городу и другому населенному пункту, выставляя нужное значение
- Выгружаем все дороги
- Ищем пересечение дорог
- Кладем все в индекс
- Ищем
Для реализации я выбрал Go, учитывая проекты типа pbf2json, golang-geo и многих других для обработки геоданных. Также хотелось покачать скилл именно в нем.
Реализация
С получением и парсингом данных с осм вроде разобрался. Для жилых массивов используем теги place=city, place=village, place=suburb, place=town, place=neighbourhood для фильтрации. Для получения адресов, зданий addr: street+addr: housenumber, amenity, shop, addr: housenumber
Все дороги можно получить с помощью тега highway
Встали сложности с поиском англоязычных названий на русском языке. Как пробовал это решить:
- Простая автоматическая транслитерация в русский. В итоге получалось абсурдной и не корректной. Пример конвертации данных был таким: City House → Цити Хоусе
- Давай попробуем преобразовывать так. Получать транскрипцию слова и ее уже транслитерировать. Получилось что-то вроде Adrenaline rush → Эрденалин Рэш. Сносно, но нужен русский акцент, типа адреналин раш.
- Подошел такой механизм. Автоматически транслитеризуем все данные, применяя словарь замен. Все-таки простая и тупая транслитерация работает сносно. Словарь наполнился в принципе быстро через несколько прогонов на данных.
С этим разобрались к этому моменту мы уже получаем данные, которые:
- Нормализированы и приведены к русскому языку
- адреса приведены к формату Страна, город, село или поселок, микрорайон или жилой массив, улица, дом
Следующая часть квеста — найти пересечения дорог. Сделал ее по быстрому и получил очень медленную реализацию, сложностью O (n^2). Как временный выход использую Postgres+postgis для нахождения пересечений, пока не нашел хорошего алгоритма для поиска пересечений.
В итоге получился хороший парсер данных с осм, который кладет данные в ElasticSearch. Который получил простое название importer
Автоматизируй это
Учитывая то, что постоянно выкачивать и создавать индексы в эластиксерче в скоре надоело, появился компонент updater. Появилась также автоматическая конфигурация в JSON формате.
Процесс выкачивания файла и импорт его в elastic search автоматизировался. Плюс к этому появилась возможность обновлять данные в эластиксерче без даунтайма, благодаря алиасам.
Как это работает:
- Updater качает файл
- Узнает текущую версию индекса с конфига
- Инкрементит версию и создает новый индекс
- Заполняет его данными
- Меняет алиасы
- Удаляет старый индекс
Получил такие бенефиты от этого:
- Пишем конфиг
- Запускаем ./ariadna update
- Идем пить кофе
- Получаем готовый настроенный индекс.
Также для удобства прикрутил простой вебинтерфейс с картой и возможностью поиска.
Автоматическое пополнение данными
Помимо ОСМ у нас еще есть много водителей и операторы, которые забивают заказы.
Соотвественно у нас есть имя и координаты
Сделана такая схема:
- Треки водителей хранятся в индексе drivers_data
- Данные с ОСМ хранятся в индексе osm_data
- Объединены они через алиас addresses по которому и происходит поиск адресов
Данные от водителей заносятся, если у нас погрешность в определенных координатах больше, чем 200 метров.
Итого
Получился геокодер который умеет:
- Искать координаты по синонимам. например ШВК — ШампанВинКомбинат
- Умеет искать адреса в определенном радиусе (например для себя с сделал поиск адресов в 30 км от центра города)
- Искать по названию заведений (кафе у Ашота например)
- Искать перекрестки
- Искать адреса в микрорайонах и жил массивах
- Делать реверс геокодинг
- Автоматически пополнятся новыми данными от водителей
Состоит из трех компонентов:
- Импортер данных
- Апдейтер данных
- Веб интерфейс
Минусы
- Протестирован только для Кыргызстана
- Нет демки
- Нет поддержки всех схем адресации
Поэтому надеюсь кто-нибудь поможет его допилить и для хорошего поиска по другим странам и городам.
Если кому-то проект показался интересным, то я не против любой критики, пул реквестов, issues на гитхабе и фидбека в целом.