Реализация определения ближайших объектов (POI) в мобильном приложении
Найти ближайший банкомат, аптеку или точку самовывоза — базовый сценарий для сотен приложений. Разница между «работает хорошо» и «тормозит и показывает устаревшее» — в архитектуре запросов и правильном использовании API.
Два подхода: клиентский и серверный поиск
Клиентский поиск — загружаем все точки (или их подмножество) в приложение, ищем ближайшие на устройстве. Работает для небольших наборов данных — до нескольких тысяч точек. Фильтрация по расстоянию через формулу Хаверсина или через CLLocation.distance(from:) / Location.distanceTo(). Плюс: работает офлайн. Минус: нельзя хранить миллион точек в памяти.
Серверный поиск — PostGIS ST_DWithin, MongoDB $near, Elasticsearch geo_distance query. Для больших датасетов только этот вариант. Приложение отправляет координаты и радиус, сервер возвращает отсортированный список.
Google Places Nearby Search
Для POI из открытых данных (кафе, банки, аптеки) — Google Places API:
GET https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=55.75,37.62&radius=1000&type=pharmacy&key=...
На iOS через GMSPlacesClient.findPlaceLikelihoodList или прямой HTTP. На Android через Retrofit. Возвращает до 20 результатов за запрос, следующая страница — через pagetoken. Важно: pagetoken активируется не сразу, нужна задержка 2 секунды перед запросом следующей страницы.
Для кастомных точек (собственные магазины, пункты выдачи) — собственный бэкенд. PostGIS запрос:
SELECT id, name, lat, lon,
ST_Distance(geom, ST_MakePoint(:lon, :lat)::geography) AS distance_m
FROM locations
WHERE ST_DWithin(geom, ST_MakePoint(:lon, :lat)::geography, :radius)
ORDER BY distance_m
LIMIT 50;
Отображение на карте и кластеризация
Если точек больше 50 на экране — нужна кластеризация. На iOS: GMSMarkerClusterer из google-maps-ios-utils. На Android: ClusterManager из android-maps-utils. В Flutter: flutter_map + flutter_map_marker_cluster.
Кластеры пересчитываются при каждом изменении зума. Без debounce на событие onCameraMove это вызывает лаг — расчёт кластеров должен происходить асинхронно, не на main thread.
При тапе на кластер — плавное масштабирование через CameraUpdate.newLatLngBounds() к границам кластера, не просто зум к центру.
Обновление при перемещении
Не перезапрашивать POI на каждое обновление геолокации. Логика: запрашиваем при первой загрузке и при перемещении пользователя более чем на N метров от центра последнего запроса (для большинства случаев — 300-500 м). CLLocation.distance(from: lastQueryCenter) > threshold.
Срок: два-четыре дня — выбор провайдера, API-интеграция, отображение с кластеризацией, логика обновления.







