DIY geocoding service

Urban Materials
5 min readSep 2, 2022

Создание сервиса геокодирования с использованием Python и Elasticsearch.

photography from unsplash

В этой статье мы рассмотрим пример построения простого сервиса по прямому геокодированию на территории города Москвы. Это руководство по разработке сервиса не претендует на единственно верное решение и в основном преследует цель познакомить читателя с технологией.

Для разработки сервиса мы будем использовать Python (3.8 или выше версии) и установочный пакет Elasticsearch (msi) версии 7.9.0. Сервис разворачивается на Windows 10.

Прямое геокодирование — это задача получения координат географической точки по её адресу, то есть при введении адреса сервис возвращает место в системе координат WGS84 (EPSG:4326).

В качестве исходной базы данных для геокодирования мы будем использовать открытые данные из сети интернет (например сервис data.mos.ru, а именно Адресный реестр объектов недвижимости города Москвы или данные из OSM). Вы можете использовать любой другой источник данных, доступный вам, и содержащий адрес и координаты зданий.

Перед началом необходимо установить Python (настроить окружение и установить необходимые библиотеки) и Elasticsearch. Проверить правильность установки Elasticsearch можно перейдя по ссылке в браузере: http://localhost:9200, в ответ вы получите:

{
"name" : "YOUR NAME",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "kuO-N2pM9FSVKQ88XoYp_w",
"version" : {
"number" : "7.9.0",
"build_flavor" : "unknown",
"build_type" : "unknown",
"build_hash" : "a479a2a67fce0389512d6a9361301778b92dff667",
"build_date" : "2020-08-31T21:36:48.204330Z",
"build_snapshot" : false,
"lucene_version" : "8.6.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}

Для загрузки данных в Elasticsearch нам необходимо обработать данные из открытых источников, определиться с системой координат в которой будут возвращаться результаты геокодирования (на примере OSM).

import geopandas as gpd
import pandas as pd
import shapely

# читаем файл с полигонами зданий из OSM (возможна реализация
# загрузки данных через Python с использованием библиотеки OSMnx)
# Система координат остается неизменной EPSG:4326
gdf_osm = gpd.read_file('building-polygon.shp', scr='EPSG:4326')
# собираем адрес
gdf_osm['adres'] = gdf_osm.A_STRT +' '+ gdf_osm.A_HSNMBR
# выбираем не пустые записи в поле adres
gdf_osm = gdf_osm[~gdf_osm['adres'].isnull()]
# сбрасываем индекс
gdf_osm.reset_index(drop=True, inplace=True)
# выбираем поля с адресом и геометрией, наш геокодер будет
# возвращать контур здания (полигон) в ответ на запрос.
gdf_osm = gdf_osm[['adres', 'geometry']]
# преобразовываем геометрию в строку
gdf_osm['geom'] = gdf_osm.geometry.apply(lambda x: shapely.wkt.dumps(x))
# создаем датафрейм для загрузки в Elasticsearch
df = pd.DataFrame(gdf_osm[['adres', 'geom']])
# добавляем колонку eventId для корректной загрузки
# (в других версиях библиотеки espandas могут быть другие имена
# обязательных полей)
df['eventId'] = df.index

Теперь создаем подключение к Elasticsearch и загружаем данные.

Для загрузки данных в базу будем использовать библиотеку Espandas, в зависимости от версии необходимо будет поправить код библиотеки, по этой ссылке описано решение проблемы: https://github.com/dashaub/espandas/issues/2

from espandas import Espandas

# назначаем индекс и тип документов
INDEX = 'geo'
TYPE = 'geos'

# создаем 'подключение'
esp = Espandas()

# записываем данные в Elasticsearch
esp.es_write(df, INDEX, TYPE)

В зависимости от количества данных время записи в БД может варьироваться.

Проведем тест на примере адреса: “улица МОСФИЛЬМОВСКАЯ дом 8

from elasticsearch import Elasticsearch
from pandas.io.json import json_normalize

# создаем 'подключение'
es = Elasticsearch('http://localhost:9200')

# адрес для теста
content = 'улица МОСФИЛЬМОВСКАЯ дом 8'

# запрос к базе:
# 'geo' - ранее указанный индекс
# 'adres' - поле по которому осуществляется поиск
# "fuzziness" - алгоритм сопоставления адресов
search_res = es.search(index='geo',
body={
'query': {
'match': {
'adres': {
'query': content,
"fuzziness": "AUTO"
},
}
}})

# ТОП-10 результатов преобразованные в датафрейм
df_res = json_normalize(search_res['hits']['hits'])

Результат работы поисковика:

Наиболее подходящий ответ находится в первой строке (ответ возвращается отсортированным по колонке “_score”, которая указывает на степень совпадения адреса подаваемого в запрос и адреса из базы данных, более подробно о составлении запроса по ссылке).

Как можно улучшить результаты поиска:

1. Добавлять новые адресные базы с геопривязкой.

2. Использовать аугментацию и очистку данных, путем создания набора слов исключений.

3. Использование специализированных библиотек для нормализации адреса (pullenti_wrapper, natasha и т.д.)

4. Отказаться от Elasticsearch и реализовать свой алгоритм сравнения ( fuzzywuzzy, levenshtein и т.д.).

Для созданию сервиса (локального) по геокодированию адресов, мы будем использовать библиотеку Streamlit. Подробно останавливаться на этом шаге не будем, приведем лишь один из рабочих вариантов реализации такого приложения. Тестовый набор данных расположен по ссылке и содержит 20 адресов для проверки работоспособности этого решения.

import pandas as pd
import streamlit as st
from elasticsearch import Elasticsearch
from pandas.io.json import json_normalize


# ini
es = Elasticsearch('http://localhost:9200')

# Functions
def elastic_search(content):
"""Функция отправляет запрос в базу и возвращает адрес и его геометрию"""
search_res = es.search(index='geo',
body={
'query': {
'match': {
'adres': {
'query': content,
"fuzziness": "AUTO"
},
}
}})
df = json_normalize(search_res['hits']['hits'])
if len(df) !=0:

return df['_source.adres'][0], df['_source.geom'][0]




def generate_csv(df, name):
"""Функция сохраняет файл по указанному пути"""
df.to_csv(f'{name}', encoding='cp1251')


# Streamlit app
st.set_page_config(page_title='geocoding_service', layout="wide", initial_sidebar_state="expanded")
st.sidebar.title('Геокодирование адресов')
st.sidebar.write('Для корректной работы программы подготовьте файл с расширением *.csv, где имя колонки с геокодируемыми адресами - ADRES')
data = st.sidebar.file_uploader("Загрузите файл с расширением csv, выберите разделитель и кодировку файла", type=([".csv"]))

try:
sep = st.sidebar.selectbox('Выберите разделитель ', ['', ";", ",","\t"])
enc = st.sidebar.selectbox('Выберите кодировку ', ['', "cp1251", "utf8"])
df = pd.read_csv(data, delimiter=sep, encoding=enc)
try:
st.write('Процесс геокодирования запущен')
if len(df) != 0:
df['source.adress'] = None
df['_source.geom'] = None
# Отправляем запрос на геокодирование для каждого адреса из нашего списка
df['lst'] = df['ADRES'].apply(lambda x: elastic_search(str(x)))
df[['source.adress', '_source.geom']] = pd.DataFrame(df.lst.tolist(), index=df.index)
del df['lst']

st.write('Результат')
st.table(df)

name = st.sidebar.text_input('Введите путь и имя файла: ', r'C:\Users\YOUR_NAME\Downloads\имя файла.csv')

if st.sidebar.button('Сохранить результат'):
generate_csv(df, name)
st.sidebar.write('Файл сохранен по адресу: ', f'{name}')
except:
st.title('Попробуйте изменить разделитель и кодировку файла.')

except:
st.stop()

Создайте файл с кодом представленным выше и расширением .py. Для запуска приложения необходимо в командной строке ввести следующую команду:

streamlit run \path\to\file\name_your_file.py

Если все выполнили правильно в окне браузера откроется вкладка geocoding_service с вашим приложением.

Демонстрация работы приложения

После выполнения процедуры геокодирования сохраните файл на своем компьютере и перейдите в настольное приложение QGIS, для того чтобы просмотреть результат геокодирования. Для этого в меню выберите Слой >Добавить слой>Добавить слой из текста с разделителями. В открывшемся меню установите значения как на скриншоте:

Результат геокодирования на карте в ПО QGIS:

В заключении хотелось бы отметить, что данное решение нацелено отдать, в результатах запроса, хоть насколько-то близкое решение к вашему адресу, это может создать определенные проблемы при геокодировании большого объема данных, поскольку придётся проводить визуальный контроль на совпадение адресов. Кроме того для корректного функционирования необходимо добавить проверки и возможно предварительно очистить и нормализовать данные, как для базы данных, так и для подаваемых адресов в запросе. Описанный выше процесс скорее является отправной точкой для построения сервиса геокодирования, для корректного функционирования которого придётся решить много интересных задач.

--

--

Urban Materials

Data analysis. Geoinformation technologies. Geodata visualization. Urban planning.