Интеграция с geocoding API

Работа с поиском географических объектов в приложениях поверх MapLibre GL JS почти всегда требует интеграции с геокодингом — сервисом преобразования текстовых запросов в координаты и обратно. В современных интерфейсах карта без поиска воспринимается как статичная визуализация, тогда как связка карты и геокодинга формирует полноценный инструмент навигации и анализа данных.

Геокодинг делится на два основных типа:

  • Forward geocoding — преобразование строки (адрес, название места) в координаты [lng, lat]
  • Reverse geocoding — преобразование координат в человекочитаемый адрес

Архитектура интеграции геокодинга с MapLibre GL JS

Типичная схема взаимодействия:

  1. Пользователь вводит запрос в поисковую строку

  2. Приложение отправляет запрос в геокодинг API

  3. API возвращает список совпадений с координатами

  4. Выбранный результат используется для управления картой:

    • центрирование (map.flyTo)
    • установка маркера
    • отображение popup
    • изменение масштаба

MapLibre GL JS не предоставляет встроенного геокодера, поэтому интеграция всегда внешняя.


Выбор геокодинг-сервиса

На практике используются несколько популярных решений:

Mapbox Geocoding API

Mapbox предоставляет мощный коммерческий API геокодинга с высокой точностью и автодополнением.

Преимущества:

  • высокая релевантность результатов
  • поддержка autocomplete
  • глобальное покрытие
  • reverse geocoding

Ограничения:

  • требуется токен доступа
  • тарифные лимиты

Nominatim (OpenStreetMap)

Nominatim — бесплатный геокодер на базе OpenStreetMap.

Преимущества:

  • бесплатное использование
  • открытые данные
  • отсутствие vendor lock-in

Ограничения:

  • строгие rate limits
  • меньшая стабильность под нагрузкой
  • необходимость собственного инстанса для production

Базовая интеграция forward geocoding

Пример реализации поиска через fetch и последующего обновления карты.

async function geocode(query) {
  const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(query)}.json` +
              `?access_token=${MAPBOX_TOKEN}&limit=5`;

  const response = await fetch(url);
  if (!response.ok) {
    throw new Error('Geocoding request failed');
  }

  const data = await response.json();
  return data.features;
}

Связывание результата с MapLibre GL JS

После получения координат выполняется обновление состояния карты:

function flyToResult(map, feature) {
  const [lng, lat] = feature.center;

  map.flyTo({
    center: [lng, lat],
    zoom: 14,
    essential: true
  });
}

Добавление маркера:

const marker = new maplibregl.Marker()
  .setLngLat([lng, lat])
  .addTo(map);

Popup с информацией:

new maplibregl.Popup()
  .setLngLat([lng, lat])
  .setHTML(`<strong>${feature.place_name}</strong>`)
  .addTo(map);

Реализация поисковой строки с autocomplete

Одной из ключевых задач является минимизация количества запросов к API. Для этого используется debounce.

function debounce(fn, delay) {
  let timeout;
  return function (...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn.apply(this, args), delay);
  };
}

Применение:

const searchInput = document.getElementById('search');

searchInput.addEventListener(
  'input',
  debounce(async (e) => {
    const results = await geocode(e.target.value);
    renderSuggestions(results);
  }, 300)
);

Отображение списка подсказок

UI-слой обычно строится отдельно от карты:

function renderSuggestions(features) {
  const container = document.getElementById('suggestions');
  container.innerHTML = '';

  features.forEach(feature => {
    const item = document.createElement('div');
    item.className = 'suggestion-item';
    item.textContent = feature.place_name;

    item.oncl ick = () => {
      flyToResult(map, feature);
      container.innerHTML = '';
    };

    container.appendChild(item);
  });
}

Reverse geocoding для координат карты

Reverse geocoding применяется при клике по карте или перемещении курсора.

async function reverseGeocode(lng, lat) {
  const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${lng},${lat}.json` +
              `?access_token=${MAPBOX_TOKEN}`;

  const response = await fetch(url);
  const data = await response.json();

  return data.features[0];
}

Интеграция с событием клика:

map.on('click', async (e) => {
  const feature = await reverseGeocode(e.lngLat.lng, e.lngLat.lat);

  new maplibregl.Popup()
    .setLngLat(e.lngLat)
    .setHTML(feature.place_name)
    .addTo(map);
});

Работа с источниками данных и слоями

Геокодинг часто используется совместно с динамическими слоями:

  • добавление маркеров как GeoJSON source
  • обновление setData при выборе результата
  • группировка результатов
map.addSource('search-result', {
  type: 'geojson',
  data: {
    type: 'FeatureCollection',
    features: []
  }
});

Обновление:

function updateSource(feature) {
  map.getSource('search-result').setData({
    type: 'FeatureCollection',
    features: [feature]
  });
}

Оптимизация запросов и кеширование

При активном использовании поиска важно снижать нагрузку на API.

Подходы:

  • локальное кеширование результатов
  • ограничение длины запроса
  • debounce/throttle
  • отмена устаревших запросов

Пример кеша:

const cache = new Map();

async function cachedGeocode(query) {
  if (cache.has(query)) {
    return cache.get(query);
  }

  const results = await geocode(query);
  cache.set(query, results);

  return results;
}

AbortController для отмены запросов

При быстром вводе необходимо отменять предыдущие запросы:

let controller;

async function geocodeCancelable(query) {
  if (controller) controller.abort();

  controller = new AbortController();

  const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${query}.json?access_token=${MAPBOX_TOKEN}`;

  const response = await fetch(url, {
    signal: controller.signal
  });

  return response.json();
}

Обработка ошибок и деградация функциональности

Типичные проблемы:

  • превышение rate limit
  • сетевые ошибки
  • пустые результаты
  • некорректный ввод

Пример обработки:

try {
  const results = await geocode(query);

  if (!results.length) {
    showMessage('Ничего не найдено');
  }
} catch (err) {
  showMessage('Ошибка поиска');
}

Геокодинг без коммерческих API

При использовании OpenStreetMap возможно прямое обращение к сервису:

async function nominatimGeocode(query) {
  const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`;

  const response = await fetch(url, {
    headers: {
      'User-Agent': 'Map Application'
    }
  });

  return response.json();
}

Reverse geocoding:

async function nominatimReverse(lng, lat) {
  const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`;

  const response = await fetch(url);
  return response.json();
}

Интеграция с состоянием карты и UI-синхронизация

Геокодинг влияет на несколько уровней приложения:

  • центр карты
  • масштаб
  • маркеры
  • активные слои
  • URL состояние (deep linking)

Пример синхронизации URL:

function updateUrl(lng, lat, zoom) {
  const url = new URL(window.location);
  url.searchParams.set('lng', lng);
  url.searchParams.set('lat', lat);
  url.searchParams.set('zoom', zoom);
  window.history.replaceState({}, '', url);
}

Использование геокодинга в сложных сценариях

Дополнительные сценарии:

  • фильтрация POI по области
  • поиск ближайших объектов после геокодинга
  • кластеризация результатов
  • комбинирование с routing API
  • построение маршрутов от найденной точки

При этом геокодинг становится не отдельной функцией, а частью общей гео-логики приложения поверх MapLibre GL JS.