Autocomplete для поиска

Autocomplete в интерфейсах геопоиска представляет собой механизм предсказания и подстановки результатов по мере ввода текста. В контексте MapLibre GL JS он чаще всего используется совместно с геокодерами и собственными UI-контролами, обеспечивая быстрый выбор географических объектов без необходимости полного ввода запроса.

Ключевая задача autocomplete в картографических приложениях — минимизировать количество запросов к серверу и одновременно повысить точность ввода за счёт подсказок, основанных на частичном совпадении строк, локальных индексах или внешних API.


Архитектура autocomplete в MapLibre GL JS

Типовая архитектура автодополнения в связке с MapLibre GL JS включает несколько слоёв:

  • UI-слой ввода: текстовое поле и список подсказок
  • Логика управления состоянием: обработка ввода, debounce, кеширование
  • Геокодер: внешнее API (Nominatim, Mapbox Geocoding, Photon, Pelias)
  • Карта MapLibre GL JS: отображение результата (marker, flyTo, fitBounds)

Связь между слоями строится асинхронно: пользователь вводит текст → выполняется debounce → отправляется запрос → возвращается список → обновляется UI → выбранный результат синхронизируется с картой.


Базовая реализация поля ввода

Основой autocomplete является контрол ввода. В MapLibre GL JS чаще всего создаётся пользовательский control через map.addControl.

class SearchControl {
  onAdd(map) {
    this.map = map;

    this.container = document.createElement('div');
    this.container.className = 'maplibre-search';

    this.input = document.createElement('input');
    this.input.type = 'text';
    this.input.placeholder = 'Поиск...';

    this.list = document.createElement('div');
    this.list.className = 'search-suggestions';

    this.container.appendChild(this.input);
    this.container.appendChild(this.list);

    this.bindEvents();

    return this.container;
  }

  onRemove() {
    this.container.parentNode.removeChild(this.container);
    this.map = undefined;
  }

  bindEvents() {
    this.input.addEventListener('input', (e) => {
      this.onInput(e.target.value);
    });
  }

  onInput(value) {
    // будет реализовано далее
  }
}

Такой подход позволяет встроить поиск как полноценный элемент управления картой.


Debounce для оптимизации запросов

Без ограничения частоты запросов autocomplete быстро перегружает API. Решение — debounce.

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

Применение в контроле:

this.onIn put = debounce((value) => {
  if (value.length < 3) {
    this.clearSuggestions();
    return;
  }
  this.fetchSuggestions(value);
}, 300);

Оптимальная задержка обычно находится в диапазоне 200–400 мс, в зависимости от скорости API и требований UX.


Запросы к геокодеру

Autocomplete почти всегда опирается на геокодинг. Пример запроса к Nominatim:

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

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

  this.renderSuggestions(data);
}

Каждый элемент ответа содержит координаты, название и тип объекта, что позволяет использовать его напрямую в MapLibre GL JS.


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

UI списка должен быть быстрым и минимально инвазивным.

renderSuggestions(items) {
  this.list.innerHTML = '';

  items.forEach((item, index) => {
    const el = document.createElement('div');
    el.className = 'suggestion-item';
    el.textContent = item.display_name;

    el.addEventListener('click', () => {
      this.selectSuggestion(item);
    });

    this.list.appendChild(el);
  });
}

Важно избегать сложной DOM-структуры: autocomplete должен оставаться лёгким даже при десятках обновлений в секунду.


Интеграция с MapLibre GL JS

После выбора результата происходит синхронизация с картой.

selectSuggestion(item) {
  const lng = parseFloat(item.lon);
  const lat = parseFloat(item.lat);

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

  new maplibregl.Marker()
    .setLngLat([lng, lat])
    .addTo(this.map);

  this.clearSuggestions();
  this.input.value = item.display_name;
}

Использование flyTo создаёт плавный переход, а маркер фиксирует выбранную точку.


Клавиатурная навигация

Autocomplete без клавиатуры считается неполноценным. Основные сценарии:

  • ArrowDown / ArrowUp — перемещение по списку
  • Enter — выбор элемента
  • Escape — закрытие списка
bindEvents() {
  this.input.addEventListener('input', (e) => {
    this.onInput(e.target.value);
  });

  this.input.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowDown') this.moveDown();
    if (e.key === 'ArrowUp') this.moveUp();
    if (e.key === 'Enter') this.selectActive();
    if (e.key === 'Escape') this.clearSuggestions();
  });
}

Поддержка активного индекса:

this.activeIndex = -1;

Состояние и управление списком

Список autocomplete требует управления состоянием:

  • текущий запрос
  • активный элемент
  • кеш результатов
  • флаг загрузки

Пример кеширования:

this.cache = new Map();

async fetchSuggestions(query) {
  if (this.cache.has(query)) {
    this.renderSuggestions(this.cache.get(query));
    return;
  }

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

  this.cache.set(query, data);
  this.renderSuggestions(data);
}

Кеширование особенно эффективно при повторяющихся запросах или медленном соединении.


Обработка ошибок и нестабильных ответов

Геокодинг может возвращать:

  • пустые результаты
  • ошибки сети
  • задержки ответа

Рекомендуемая стратегия — мягкая деградация:

async fetchSuggestions(query) {
  try {
    const response = await fetch(url);
    if (!response.ok) throw new Error('Network error');

    const data = await response.json();
    this.renderSuggestions(data);
  } catch (e) {
    this.renderSuggestions([]);
  }
}

UI должен оставаться стабильным даже при отсутствии данных.


Производительность и масштабирование

При высокочастотном вводе критичны следующие оптимизации:

  • debounce запросов
  • ограничение длины запроса (например, ≥ 3 символов)
  • виртуализация списка при > 20–30 элементов
  • кеширование результатов
  • отмена устаревших запросов через AbortController

Пример отмены запросов:

this.controller = new AbortController();

fetch(url, { signal: this.controller.signal });

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

if (this.controller) this.controller.abort();
this.controller = new AbortController();

UX-паттерны для autocomplete в картах

В картографических интерфейсах применяются специфические паттерны:

  • при выборе результата карта всегда центрируется
  • подсказки содержат тип объекта (город, улица, POI)
  • при пустом вводе список скрывается
  • повторный выбор обновляет маркер, а не создаёт новый
  • результаты сортируются по релевантности и расстоянию

Эти правила позволяют избежать визуального шума и улучшить предсказуемость поведения интерфейса.


Стилизация интерфейса

CSS играет критическую роль в восприятии autocomplete:

.maplibre-search {
  position: absolute;
  top: 10px;
  left: 10px;
  width: 300px;
  background: white;
}

.search-suggestions {
  max-height: 240px;
  overflow-y: auto;
}

.suggestion-item {
  padding: 8px;
  cursor: pointer;
}

Важным аспектом является абсолютное позиционирование относительно карты и предотвращение перекрытия ключевых элементов управления.


Интеграция с кастомными геокодерами

MapLibre GL JS не ограничивает выбор backend-решения. Часто используются:

  • Pelias
  • Photon
  • Algolia Places (устаревшие реализации)
  • собственные индексы Elasticsearch

Абстракция слоя запроса позволяет переключать backend без изменения UI-логики:

class Geocoder {
  async search(query) {
    return fetch(`/api/geocode?q=${query}`).then(r => r.json());
  }
}

Такой подход упрощает масштабирование и локализацию поиска.


Поведение при географических границах

Autocomplete может учитывать текущий viewport карты:

  • bias по bounding box
  • сортировка по расстоянию до центра карты
  • ограничение результатов по региону

Пример передачи bbox:

const bbox = this.map.getBounds().toArray().flat();

const url = `/search?q=${query}&bbox=${bbox.join(',')}`;

Это значительно повышает релевантность результатов в локальных сценариях использования карты.