Статья будет полезна тем, кто желает немного обогатить имеющиеся геоданные:
Однажды мне понадобился гео-API для нахождения координаты по строке с адресом, и сделать свой для меня стало целесообразно из-за того, что попадавшиеся мне готовые решения по тем или иным причинам не подходили - регистрация, оплата, ограничения на количество запросов в минуту/час/сутки и т.д. + обработка данных с внешними сервисами займет значительно больше времени при большом количестве записей, т.к. зачастую время выполнения запроса с внешними сервисами больше, чем с локальными.
Оговорюсь, что исходные адресные данные у меня были разделены по полям "район", "улица", "дом", и в процессе своей работы меня интересовали данные только в рамках одного города - СПб, и для примера здесь я буду использовать данные по СПб.
Вероятно, для Москвы подобное делать будет немного подольше ввиду более сложного административного деления. По остальным населенным пунктам не могу сказать.
Также, стоит заметить, что сообщество OSM не очень активно (РУ особенно, по ощущениям), и поэтому информация в OSM не всегда достаточно актуальная, иногда старые дома могут годами оставаться неразмеченными на карте, не говоря уже новых.
Как вы уже догадались, нам понадобится Linux-сервер с установленным Elasticsearch. Также надо установить Kibana для визуализаций и для Dev Tools с запросником. Нужно установить пакет jq.
Дальше мы возьмем информацию из OSM и обработаем ее, чтобы пользоваться ей локально.
1) Получим информацию о домах из OSM
Мы сможем это сделать с помощью специального Overpass API, который позволяет за раз выгружать много данных, удовлетворяющих запросу.
Почитать про Overpass API: https://wiki.openstreetmap.org/wiki/RU:Overpass_API
Попробовать сделать запрос: https://overpass-turbo.eu/
Немного подождав мы получим файл osm_spb_buildings.json примерно такого содержания:
2) Приводим файл в удобоваримое для Elasticsearch состояние, попутно делая пару улучшений.
Чуть позже с помощью Bulk API мы импортируем сразу все здания одним запросом. Иначе, делая >100к запросов время импорта будет значительно больше.
Сделаем из полученного в предыдущем пункте файла новый файл, который будет содержать в себе множество запросов на добавление зданий.
Почитать про Bulk API: https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
После ./json-to-bulk-json.sh ./json/osm_spb_buildings.json на выходе у нас будет файл ./bulk-json/osm_spb_buildings.json.bulk.json:
3) Создаем маппинг в Elasticsearch.
Перед добавлением данных лучше всегда задавать маппинг, т.к. анализатор Elasticsearch почти всегда не угадывает тип поля и проставляет по-умолчанию тип "text".
В нашем случае, самое главное - заранее назначить полю "center" тип "geo_point".
4) Импортируем здания.
(./bulk-json-to-elasticsearch.sh ./bulk-json/osm_spb_buildings.json.bulk.json)
Теперь надо подождать, когда закончится отправка запроса, и потом дождаться завершения индексации.
В Kibana, в разделе "Stack Management - Index Management" можно увидеть количество записей в столбеце "Docs count" у индекса "osm_spb_buildings".
5) Заполняем у домов поле с названием района.
Порядок действий: создаем маппинг индекса с границами районов -> заполняем этот индекс -> для всех домов, входящих в границы каждого района заполняем поле "district" (соответствующее название района) и "region" (Санкт-Петербург).
id районов взял из OSM Вики: https://wiki.openstreetmap.org/wiki/RU:Санкт-Петербург/Районы
И доставал границы такими запросами, подставив id: https://nominatim.openstreetmap.org...R&osmid=1114193&polygon_geojson=1&format=json
Готово! =)
Теперь можно приступить к использованию.
Пример запроса по адресу:
Пример запроса по координате (distance - радиус поиска):
- добавить поле с координатой при наличии города-района-улицы-дома-корпуса
- или, наоборот, по координате получить строку с адресом.
Однажды мне понадобился гео-API для нахождения координаты по строке с адресом, и сделать свой для меня стало целесообразно из-за того, что попадавшиеся мне готовые решения по тем или иным причинам не подходили - регистрация, оплата, ограничения на количество запросов в минуту/час/сутки и т.д. + обработка данных с внешними сервисами займет значительно больше времени при большом количестве записей, т.к. зачастую время выполнения запроса с внешними сервисами больше, чем с локальными.
Оговорюсь, что исходные адресные данные у меня были разделены по полям "район", "улица", "дом", и в процессе своей работы меня интересовали данные только в рамках одного города - СПб, и для примера здесь я буду использовать данные по СПб.
Вероятно, для Москвы подобное делать будет немного подольше ввиду более сложного административного деления. По остальным населенным пунктам не могу сказать.
Также, стоит заметить, что сообщество OSM не очень активно (РУ особенно, по ощущениям), и поэтому информация в OSM не всегда достаточно актуальная, иногда старые дома могут годами оставаться неразмеченными на карте, не говоря уже новых.
Как вы уже догадались, нам понадобится Linux-сервер с установленным Elasticsearch. Также надо установить Kibana для визуализаций и для Dev Tools с запросником. Нужно установить пакет jq.
Дальше мы возьмем информацию из OSM и обработаем ее, чтобы пользоваться ей локально.
1) Получим информацию о домах из OSM
Мы сможем это сделать с помощью специального Overpass API, который позволяет за раз выгружать много данных, удовлетворяющих запросу.
Почитать про Overpass API: https://wiki.openstreetmap.org/wiki/RU:Overpass_API
Попробовать сделать запрос: https://overpass-turbo.eu/
Bash:
#!/bin/bash
mkdir ./json 2>/dev/null
curl 'https://overpass-api.de/api/interpreter' -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' --data-raw 'data=[out:json][bbox:59.4,29.1,60.4,31.6];(node[building];relation[building];way[building];);out+center;' >./json/osm_spb_buildings.json
[out:json] -вывод в формате json
[bbox:59.4,29.1,60.4,31.6]; - рамка из которой будут извлекаться объекты (последовательность координат: минимальная широта, минимальная долгота, максимальная широта, максимальная долгота).
(node[building];relation[building];way[building]; ) - указывает, что будут возвращены все типы базовых элементов (точки, линии, отношения), обозначающие здания.
out+center; - кроме стандартной информации вернется координата центра каждого здания.
[bbox:59.4,29.1,60.4,31.6]; - рамка из которой будут извлекаться объекты (последовательность координат: минимальная широта, минимальная долгота, максимальная широта, максимальная долгота).
(node[building];relation[building];way[building]; ) - указывает, что будут возвращены все типы базовых элементов (точки, линии, отношения), обозначающие здания.
out+center; - кроме стандартной информации вернется координата центра каждого здания.
2) Приводим файл в удобоваримое для Elasticsearch состояние, попутно делая пару улучшений.
Чуть позже с помощью Bulk API мы импортируем сразу все здания одним запросом. Иначе, делая >100к запросов время импорта будет значительно больше.
Сделаем из полученного в предыдущем пункте файла новый файл, который будет содержать в себе множество запросов на добавление зданий.
Почитать про Bulk API: https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
Bash:
#!/bin/bash
IFS=$'\n'
if [ "$#" -eq "1" ]
then
mkdir ./bulk-json 2>/dev/null
json_path=$1
bulk_json_path=./bulk-json/$(echo $json_path | rev | cut -d'/' -f 1 | rev).bulk.json
echo ""
echo "input: $json_path"
echo "output: $bulk_json_path"
echo ""
for row in $(cat $json_path | jq -c '.elements[] | select(.tags."addr:housenumber" != null) | select(.tags."addr:street" != null) | select(.center != null) | {id} + {center} + (.tags|{"addr:street"}) + (.tags|{"addr:housenumber"}) + (.tags|{"addr:letter"}) + (.tags|{"addr:city"}) + (.tags|{"addr:suburb"}) + (.tags|{"addr:region"}) + (.tags|{"addr:place"}) + (.tags|{"addr:district"}) + (.tags|{"addr:subdistrict"}) + (.tags|{"addr:province"}) | with_entries( select( .value != null ) )')
do
echo '{ "index":{} }' >>$bulk_json_path
echo $row >>$bulk_json_path
done
sed -i -e 's/"addr:/"/g' $bulk_json_path
sed -i -e 's/Ё/Е/g' $bulk_json_path
sed -i -e 's/ё/е/g' $bulk_json_path
else
echo "1 argument expected"
fi
Скрипт в аргументе принимает путь до файла, полученного в предыдущем пункте, после отработки в каталоге ./bulk-json появляется файл, пригодный для импорта в Elasticsearch.
Каждый объект выравнивается в строку, далее остаются только объекты с заполненными полями street+housenumber+center, выводятся эти и еще некоторые поля, удаляются пустые поля.
Перед каждым объектом добавляется "{ "index":{} }"
Из названий полей удаляется бесполезное для нас "addr:"
Все "ё" меняются на "е" в целях улучшения поиска, т.к. часто в исходных данных могут встречаться разные написания улиц, прим.: "Берёзовая/Березовая" улица. (в исходных данных было проделано то же: ё -> е)
Каждый объект выравнивается в строку, далее остаются только объекты с заполненными полями street+housenumber+center, выводятся эти и еще некоторые поля, удаляются пустые поля.
Перед каждым объектом добавляется "{ "index":{} }"
Из названий полей удаляется бесполезное для нас "addr:"
Все "ё" меняются на "е" в целях улучшения поиска, т.к. часто в исходных данных могут встречаться разные написания улиц, прим.: "Берёзовая/Березовая" улица. (в исходных данных было проделано то же: ё -> е)
3) Создаем маппинг в Elasticsearch.
Перед добавлением данных лучше всегда задавать маппинг, т.к. анализатор Elasticsearch почти всегда не угадывает тип поля и проставляет по-умолчанию тип "text".
В нашем случае, самое главное - заранее назначить полю "center" тип "geo_point".
Bash:
#!/bin/bash
elasticsearch_host=0.0.0.0
curl -X PUT "$elasticsearch_host:9200/osm_spb_buildings" | jq '.'
curl -X PUT "$elasticsearch_host:9200/osm_spb_buildings/_mapping/" -H 'Content-Type: application/json' -d'{
"properties": {
"center": {
"type": "geo_point"
}
}
}' | jq '.'
4) Импортируем здания.
(./bulk-json-to-elasticsearch.sh ./bulk-json/osm_spb_buildings.json.bulk.json)
Bash:
#!/bin/bash
#set -x
IFS=$'\n'
elasticsearch_host=0.0.0.0
bulk_json_path=$1
if [ "$#" -eq "1" ]
then
echo ""
echo "file: $(echo $bulk_json_path | rev | cut -d'/' -f 1 | rev)"
echo ""
printf "\n\n$(date --rfc-3339=seconds)\n$bulk_json_path\n" >> import.log
curl -s -XPOST $elasticsearch_host:9200/osm_spb_buildings/_bulk -H 'Content-Type: application/json' --data-binary @$bulk_json_path | jq '.' >> import.log
else
echo "1 argument expected"
fi
В Kibana, в разделе "Stack Management - Index Management" можно увидеть количество записей в столбеце "Docs count" у индекса "osm_spb_buildings".
5) Заполняем у домов поле с названием района.
Порядок действий: создаем маппинг индекса с границами районов -> заполняем этот индекс -> для всех домов, входящих в границы каждого района заполняем поле "district" (соответствующее название района) и "region" (Санкт-Петербург).
id районов взял из OSM Вики: https://wiki.openstreetmap.org/wiki/RU:Санкт-Петербург/Районы
И доставал границы такими запросами, подставив id: https://nominatim.openstreetmap.org...R&osmid=1114193&polygon_geojson=1&format=json
Bash:
#!/bin/bash
elasticsearch_host=0.0.0.0
curl -X PUT "$elasticsearch_host:9200/osm_spb_districts" | jq '.'
curl -X PUT "$elasticsearch_host:9200/osm_spb_districts/_mapping/" -H 'Content-Type: application/json' -d'{
"properties": {
"geometry": {
"type": "geo_shape"
}
}
}' | jq '.'
echo Адмиралтейский
curl -s -XPOST $elasticsearch_host:9200/osm_spb_districts/_doc -H 'Content-Type: application/json' -d'
{ "name": "Адмиралтейский район","geometry": { "type": "Polygon","coordinates":[[[30.2502295,59.9012907],[30.2511227,59.9010298],[30.2529959,59.9004065],[30.2536453,59.9002229],[30.254016,59.9001698],[30.2545032,59.9002996],[30.2550207,59.9004425],[30.2556283,59.9005558],[30.2561513,59.9005716],[30.2570052,59.900495],[30.2575727,59.9003963],[30.2611487,59.8998644],[30.2621002,59.8998911],[30.262968,59.900082],[30.2646122,59.9005706],[30.2650538,59.90096],[30.2657085,59.901819],[30.26615,59.9019564],[30.2666828,59.901964],[30.2676695,59.9020115],[30.2681268,59.902092],[30.2686119,59.9022005],[30.2689758,59.9023113],[30.2690959,59.9024091],[30.269288,59.9023757],[30.2694215,59.9023548],[30.269548,59.9023345],[30.269637,59.9023163],[30.2703424,59.9021777],[30.2710687,59.9020349],[30.2713176,59.9019862],[30.271593,59.9019331],[30.2719329,59.9018665],[30.2719736,59.9018586],[30.2725453,59.9017448],[30.2727358,59.9017071],[30.2728677,59.9016808],[30.2729147,59.9017113],[30.272967,59.9017361],[30.2730218,59.9017564],[30.2730901,59.9017773],[30.2731614,59.9017919],[30.2732405,59.9018017],[30.2733141,59.9018049],[30.2733895,59.9018034],[30.2734591,59.9017971],[30.2735274,59.9017871],[30.2735963,59.9017715],[30.273656,59.9017526],[30.2737201,59.9017268],[30.273768,59.901696],[30.2738068,59.9016667],[30.2738418,59.9016322],[30.273872,59.9015835],[30.2738823,59.9015438],[30.2738776,59.9015061],[30.2739961,59.9015009],[30.2741066,59.9014956],[30.2742053,59.9014945],[30.2742969,59.9014986],[30.2743874,59.9015116],[30.2744755,59.9015275],[30.2745826,59.9015633],[30.2758289,59.901114],[30.2763414,59.9013598],[30.276802,59.9016308],[30.2767748,59.9016446],[30.2769779,59.9017701],[30.2771023,59.9018461],[30.27724,59.9019409],[30.2774615,59.9020959],[30.2776363,59.9022725],[30.2796349,59.9033556],[30.2800151,59.9035588],[30.2801824,59.9035934],[30.2803545,59.9036369],[30.2807081,59.9037477],[30.2809136,59.9037968],[30.2811581,59.9038415],[30.2814008,59.9038761],[30.2819152,59.9039237],[30.2821248,59.903935],[30.283617,59.9038043],[30.2845809,59.9037106],[30.2866383,59.9035471],[30.2890871,59.9033022],[30.2899085,59.9032431],[30.2899133,59.903178],[30.2905912,59.9031405],[30.2907441,59.9031252],[30.2908979,59.9031098],[30.2911294,59.9031296],[30.2920476,59.903078],[30.2927639,59.9029629],[30.2930904,59.9028945],[30.2931241,59.9029252],[30.2934545,59.9028647],[30.293935,59.902771],[30.2942298,59.9027354],[30.2942624,59.9027989],[30.2946082,59.9027544],[30.2946438,59.9027466],[30.2946088,59.9026776],[30.2947606,59.9025213],[30.2949666,59.9025689],[30.2950397,59.9025016],[30.295407,59.9026035],[30.2956998,59.9023201],[30.2956591,59.9023079],[30.2956415,59.902244],[30.2955046,59.9022039],[30.2955699,59.9021427],[30.2954743,59.902116],[30.2957213,59.9018932],[30.2957365,59.9018974],[30.2957903,59.9018642],[30.2958444,59.9017993],[30.2961957,59.9018731],[30.2962863,59.9018921],[30.2965218,59.9019645],[30.2966036,59.901988],[30.2966693,59.9019901],[30.2967364,59.9019578],[30.2968544,59.9019443],[30.2968799,59.9019373],[30.2968993,59.9019077],[30.2969395,59.9017012],[30.2970058,59.9008783],[30.2970343,59.9008787],[30.2970541,59.9005573],[30.2970287,59.9003672],[30.2970086,59.9002865],[30.2969563,59.900265],[30.2967994,59.8999717],[30.2967815,59.8999404],[30.2966638,59.8996998],[30.2966117,59.8995862],[30.2965069,59.8992927],[30.2965098,59.8991609],[30.2964566,59.8991603],[30.2963841,59.8991595],[30.2963556,59.8989089],[30.2961814,59.8986841],[30.2957445,59.8980067],[30.2952848,59.8972204],[30.2953505,59.8972104],[30.2951106,59.8967722],[30.2950022,59.8965579],[30.2951139,59.8965448],[30.2949784,59.8962945],[30.2947812,59.8961229],[30.2978907,59.8960308],[30.3001883,59.8961298],[30.3004041,59.8961338],[30.3007921,59.8961409],[30.3017053,59.8961576],[30.3026068,59.8961863],[30.3036287,59.8962009],[30.3038429,59.896204],[30.3039698,59.8962058],[30.3053317,59.8962252],[30.3053982,59.8962262],[30.3060814,59.8962359],[30.3067253,59.8962451],[30.3071072,59.8962506],[30.3074927,59.8962561],[30.3078111,59.8962606],[30.308691,59.8962732],[30.3093163,59.8962821],[30.3095353,59.8962853],[30.3098413,59.8962896],[30.30996,59.8962889],[30.3102009,59.8962901],[30.3103169,59.8962879],[30.3104101,59.896286],[30.3105069,59.8962809],[30.3106097,59.896267],[30.3129922,59.8962891],[30.3139128,59.8962977],[30.3140264,59.8963208],[30.3141313,59.896331],[30.31447,59.8963426],[30.3146793,59.8963525],[30.3149543,59.8963655],[30.3155086,59.8963854],[30.3166085,59.8963995],[30.3174361,59.8964056],[30.3184733,59.896413],[30.318643,59.8964135],[30.3186865,59.8964157],[30.3190301,59.896423],[30.3191434,59.8964256],[30.3191757,59.8964261],[30.3188175,59.9023867],[30.3186584,59.9027619],[30.3184023,59.9081949],[30.3183963,59.9085089],[30.3184052,59.9086376],[30.3195008,59.908642],[30.3201317,59.9086962],[30.3207753,59.9087756],[30.3219512,59.9091147],[30.3251527,59.9103327],[30.3325755,59.9130514],[30.3335212,59.9133749],[30.3394365,59.9155014],[30.3400733,59.9156876],[30.3401333,59.9161085],[30.3405231,59.9162781],[30.3407121,59.9163595],[30.340973,59.9164669],[30.3412284,59.9165592],[30.3421574,59.9168975],[30.3425304,59.9170392],[30.342732,59.9171158],[30.3425712,59.9172392],[30.3421995,59.9175534],[30.3420227,59.9177016],[30.3418488,59.9178448],[30.341555,59.9180866],[30.3413606,59.9182466],[30.3408572,59.918661],[30.340578,59.9188907],[30.3397238,59.9195937],[30.3396136,59.9196886],[30.3394833,59.9198042],[30.3393791,59.9198848],[30.338235,59.9208295],[30.3381155,59.9209282],[30.3379449,59.9210815],[30.337345,59.9215649],[30.3366892,59.922106],[30.3365825,59.922193],[30.3364684,59.9222873],[30.3362309,59.9224866],[30.3352309,59.9233162],[30.3357858,59.9235368],[30.3304534,59.9260735],[30.3300187,59.9262994],[30.3270334,59.925151],[30.3267011,59.9253692],[30.326572,59.925454],[30.318983,59.93053],[30.3178175,59.9313099],[30.3153107,59.9329953],[30.3118605,59.9353013],[30.3117762,59.9353581],[30.311539,59.9355176],[30.3115121,59.9355356],[30.3114609,59.9355701],[30.3113239,59.9356623],[30.3112298,59.9357255],[30.3109312,59.9359263],[30.3108416,59.9359866],[30.3105593,59.9361765],[30.3103119,59.9363429],[30.3102313,59.9363971],[30.3101295,59.9364655],[30.3103024,59.9366458],[30.3123914,59.9373876],[30.3124162,59.9374581],[30.3126331,59.9379477],[30.3126379,59.9380088],[30.3126288,59.9380613],[30.3126137,59.9381018],[30.3125887,59.9381449],[30.3125619,59.9381783],[30.3125203,59.9382183],[30.3124762,59.9382558],[30.3124315,59.9382896],[30.309989,59.9398884],[30.3090786,59.9405117],[30.308274,59.9410565],[30.2983909,59.9374942],[30.285014,59.9338202],[30.2817969,59.9330639],[30.2773869,59.9320271],[30.2724587,59.9287347],[30.2707173,59.9256064],[30.2690368,59.9225872],[30.2677802,59.9213203],[30.2653115,59.918831],[30.2624446,59.9173952],[30.2621175,59.9173546],[30.2604927,59.9172603],[30.261523,59.9169571],[30.2620661,59.9164361],[30.2621519,59.9158408],[30.2622007,59.9136743],[30.2619127,59.912359],[30.2609047,59.9107547],[30.2599408,59.9093491],[30.2576418,59.9068884],[30.2575405,59.9067851],[30.2560605,59.9049665],[30.2559255,59.9048007],[30.2553848,59.9041362],[30.2535127,59.9026841],[30.2502783,59.9013188],[30.2502295,59.9012907 ] ] ] } }
' | jq '.'
echo "Извините, остальные районы не влезли в текст топика =)"
sleep 123
Bash:
#!/bin/bash
#set -x
IFS=$'\n'
elasticsearch_host=0.0.0.0
districts="$(curl -s -XGET "http://$elasticsearch_host:9200/osm_spb_districts/_search" -H 'Content-Type: application/json' -d'{ "size": 1000, "_source": "name", "query": { "match_all": {} }}' | jq -c '.hits.hits[]')"
districts_size=$(echo "$districts" | wc -l)
echo ""
echo "districts: $districts_size"
echo ""
echo ""
for district in $districts
do
district_id=$(echo $district | jq -r '._id')
echo "id: $district_id"
district_name=$(echo $district | jq -r '._source.name')
echo "name: $district_name"
curl -s -XPOST "http://$elasticsearch_host:9200/osm_spb_buildings/_update_by_query" -H 'Content-Type: application/json' -d'{
"query": {
"bool": {
"filter": [
{
"geo_shape": {
"center": {
"indexed_shape": {
"index": "osm_spb_districts",
"id": "'$district_id'",
"path": "geometry"
}
}
}
}
]
}
},
"script": {
"inline": "ctx._source.district = \"'$district_name'\"; ctx._source.region = \"'Санкт-Петербург'\";",
"lang": "painless"
}
}' | jq '.'
echo ""
echo ""
done
Готово! =)
Теперь можно приступить к использованию.
Пример запроса по адресу:
Пример запроса по координате (distance - радиус поиска):