О POI

2020-03-28

POI — point of interest. Интересное место. Это такие точки на карте, которые обозначают местоположение чего-нибудь интересного. Кинотеатры, отели, магазины, общественные туалеты... Много чего может быть интересно в разные моменты времени.

Именно POI показываются на карте тем самым значком, когда мы что-то на этой карте ищем.

POI

Вот понадобилось на одном проекте искать все интересные места на видимой части карты. Понятно, что для этого нужно использовать какое-нибудь API каких-нибудь карт. Заказчики денег не жалеют и уже используют Google. Посмотрим, что он предлагает...

У Google обнаруживается Places API. Вроде, всё в порядке. Чтобы найти места в определённом радиусе от определённой точки, можно сделать Nearby Search request. Удобнее, конечно, ограничивать площадь поиска по bounding box, но и окружность сойдёт.

$ http GET https://maps.googleapis.com/maps/api/place/nearbysearch/json \
key==API_KEY location==54.9842888,73.3631788 radius==500

HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 6299
Content-Type: application/json; charset=UTF-8
Date: Sat, 28 Mar 2020 06:04:54 GMT
Expires: Sat, 28 Mar 2020 06:09:54 GMT
Server: scaffolding on HTTPServer2

{
    "next_page_token": "NEXT_PAGE_TOKEN",
    "results": [
        {
            "geometry": {
                "location": {
                    "lat": 54.9913545,
                    "lng": 73.3645204
                },
                "viewport": {
                    "northeast": {
                        "lat": 55.141854,
                        "lng": 73.61482389999999
                    },
                    "southwest": {
                        "lat": 54.8298979,
                        "lng": 73.097454
                    }
                }
            },
            "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/geocode-71.png",
            "id": "2533ef1675b72d86b7646cf51d20fd8ef74d0bca",
            "name": "Omsk",
            "photos": [
                {
                    "height": 2861,
                    "html_attributions": [
                        "<a href=\"https://maps.google.com/maps/contrib/101400785569019631853\">Fennec Elisabeth</a>"
                    ],
                    "photo_reference": "CmRaAAAA9y9lzKD-infmtztK1LdE5dimA-NJjpXX1TgHetgncFWclhVQfwWjHi5XjVah_TntrV2hPgYZMhReW_rBE-i2gkBulP_MpF6q4KUQu-NS2-pUh_cyGn16S56J7kzIhQ-7EhAoeK6ek6Ble2W2_YkJOVP8GhS9uDOoxe7lq0u5Oo8itSWT-q4lKA",
                    "width": 4288
                }
            ],
            "place_id": "ChIJCwkB9uL9qkMRGpumYTjD714",
            "reference": "ChIJCwkB9uL9qkMRGpumYTjD714",
            "scope": "GOOGLE",
            "types": [
                "locality",
                "political"
            ],
            "vicinity": "Omsk"
        },
...

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

Все типы мест перечислены в документации. Вполне достойный набор.

Если нужно найти интересные места по некоторому текстовому запросу, можно воспользоваться Text Search request. Примерно то же самое, только ещё нужно передать query. Выхлоп тоже точно такой же.

$ http GET https://maps.googleapis.com/maps/api/place/textsearch/json \
key==API_KEY query==магазин location==54.9842888,73.3631788 radius==500

Какие есть ограничения?

Оба запроса возвращают не более 20 результатов на странице. Следующую страницу можно заполучить, передав next_page_token из предыдущего ответа в виде параметра pagetoken.

$ http GET https://maps.googleapis.com/maps/api/place/textsearch/json \
key==API_KEY pagetoken==NEXT_PAGE_TOKEN

Всего не более трёх страниц. То есть не более 60 интересных точек. В принципе, если нет необходимости найти обязательно все кинотеатры в округе, а лишь показать, что тут есть самого важного поблизости, вполне достаточно. Больше маркеров на карте всё равно будут сливаться.

Максимальный радиус поиска — 50 километров. В такой круг поместится три-четыре Омска. Имхо, более чем достаточно. На бо́льших территориях меньше смысла искать какие-то конкретные места. Там, скорее, нужно искать, где находятся признаки цивилизации вообще. К тому же, интересных точек на таких территориях потенциально сильно много будет. Посмотрите поведение самих Google Maps. Поиск интересных мест, как правило, всё равно приблизит центр карты и будет искать уже там.

Сколько это стоит?

О. Вот это интересно. Есть плата за каждый запрос. Тысяча Nearby или Text Search запросов стоит $32. Но ещё есть плата за возвращаемые данные. Есть Basic Data: название, местоположение, адрес, иконка, только то, что мне надо — это бесплатно. Есть Contact Data: телефоны, сайт, часы работы — $3 за тысячу запросов. Есть Atmosphere Data: уровень цен, рейтинги и отзывы — $5 за тысячу запросов.

В некоторых запросах, например в Find Place, можно указать, какие данные возвращать. И уложиться в Basic Data для экономии. Find Place хорош, он даже умеет искать не в радиусе от точки, а в прямоугольнике. Но вот только он возвращает ровно одно интересное место. А мне надо много.

А вот в Nearby и Text Search нельзя ничего выбрать. И придётся платить и за Contact Data, и за Atmosphere Data, даже если они вам не нужны. То есть $40 за тысячу запросов. Это что, жадность Google, дополнительно $8 содрать?

Уже неприятно, да? А теперь — ягодки.

Читаем правила пользования Google Maps. Пункт 3.2.4. No Scraping, No Caching, No Creating Content From Google Maps Content, No Re-Creating Google Products or Features, No Use With Non-Google Maps... Вы не можете как-либо анализировать полученные данные. Вы не можете закэшировать полученные результаты. Вы не можете показывать эти данные на других картах. Вы ничего не можете, кроме как сделать запрос и выдать результат как он есть.

Ну как минимум закэшировать же хотелось. Чтобы ускорить и сэкономить. Но нет. Низя.

Альтернатива? OpenStreetMap, конечно же.

В OpenStreetMap нет явного понятия POI. Для моих целей наиболее подходят узлы (nodes, то есть одиночные точки на карте), у которых указан тег amenity (буквально «удобство»). Также имеет смысл выбирать только узлы с непустым тегом name.

По этим условиям можно сделать запрос в Overpass API. Эти API в интернетах есть, публичные и бесплатные.

$ http -f POST https://lz4.overpass-api.de/api/interpreter data='
[out:json][timeout:25];
// gather results
(
  // node with any amenity and non-empty name in the bounding box
  node["amenity"]["name"](54.97537237245619,73.35438251495361,54.99824743822034,73.38129043579102);
);
// print results
out body;
'

HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 10283
Content-Type: application/json
Date: Sat, 28 Mar 2020 07:42:06 GMT
Server: Apache/2.4.18 (Ubuntu)

{
    "elements": [
        {
            "id": 946451869,
            "lat": 54.9932212,
            "lon": 73.3724576,
            "tags": {
                "amenity": "cafe",
                "name": "Компот"
            },
            "type": "node"
        },
        {
            "id": 976571444,
            "lat": 54.9754806,
            "lon": 73.3751452,
            "tags": {
                "amenity": "restaurant",
                "name": "Сенкевич"
            },
            "type": "node"
        },
        {
            "id": 988613614,
            "lat": 54.9981718,
            "lon": 73.3556977,
            "tags": {
                "alt_name": "Kentucky Fried Chicken",
                "amenity": "fast_food",
                "brand": "KFC",
                "brand:wikidata": "Q524757",
                "brand:wikipedia": "en:KFC",
                "cuisine": "chicken",
                "int_name": "KFC",
                "name": "KFC",
                "name:de": "KFC",
                "name:en": "KFC",
                "old_name": "Ростик’с-KFC / Ростикс",
                "opening_hours": "Mo-Su 08:00-24:00",
                "takeaway": "yes",
                "website": "http://www.kfc.ru/"
            },
            "type": "node"
        },
...

Рейтингов и фоточек, конечно, нет. Зато есть координаты, имя и тип. Вполне достаточно.

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

Можно поискать и определённое amenity, где имя содержит определённую подстроку.

$ http -f POST https://lz4.overpass-api.de/api/interpreter data='
[out:json][timeout:25];
(
  node["amenity"="cafe"]["name"~"питер",i](54.97537237245619,73.35438251495361,54.99824743822034,73.38129043579102);
);
out body;
'

HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 397
Content-Type: application/json
Date: Sat, 28 Mar 2020 07:58:31 GMT
Server: Apache/2.4.18 (Ubuntu)

{
    "elements": [
        {
            "id": 4349641418,
            "lat": 54.9851388,
            "lon": 73.3743819,
            "tags": {
                "amenity": "cafe",
                "cocktails": "yes",
                "name": "Питер@Pan",
                "opening_hours": "Mo-Th, Su 11:00-01:00; Fr, Sa 11:00-03:00",
                "outdoor_seating": "terrace"
            },
            "type": "node"
        }
    ],
    "generator": "Overpass API 0.7.56.2 b688b00f",
    "osm3s": {
        "copyright": "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.",
        "timestamp_osm_base": "2020-03-28T07:58:02Z"
    },
    "version": 0.6
}

Прелесть OpenStreetMap в том, что можно поднять свой Overpass API. Или можно просто скачать planet.osm, ну точнее интересующие кусочки земной поверхности, самостоятельно пропарсить и проиндексировать, а потом обновлять индекс по регулярным диффам. И будет локальная база POI, такая быстрая, как надо.

P.S. Команда http — это HTTPie. Это как curl, но удобнее для ручного тыкания API.

P.P.S. Спасибо Артёму за внимание к пользовательским соглашениям.