Оптимизация ввода адреса: автодополнение и кэширование
Представьте: пользователь вводит «Тверск», система за 300 мс предлагает 5 вариантов. Без правильной реализации каждое нажатие — отдельный запрос к API, 70% пользователей бросают форму при задержках более 2 секунд. Среднее время ввода адреса с автодополнением — 8 секунд, без него — 35 секунд (по данным Nielsen Norman Group). Техническая сложность — не просто прикрутить библиотеку, а выбрать провайдера, настроить дебаунс, кэш и session token. Разберём, как это сделать правильно.
Как выбрать провайдера для геокодирования?
| Провайдер | Сильные стороны | Слабые стороны |
|---|---|---|
| Google Places Autocomplete API | Лучшее покрытие глобально, POI, бизнесы | Дорого при высоком трафике, слабее по корпусам в РФ |
| DaData | Лучший по адресам России (ФИАС/КЛАДР), точность >95% | Только РФ |
| Nominatim (OpenStreetMap) | Бесплатно, глобально | Нет SLA, медленнее, хуже по качеству на 30% vs DaData |
| HERE Geocoding | Хорошо в Европе, есть офлайн-пакеты | Дороже Google для малых объёмов |
| Yandex Geocoder | Хорошо по СНГ | Требует аккаунт, ограничения по условиям |
Для большинства российских проектов мы рекомендуем связку DaData + Google: DaData как первый приоритет, Google как fallback для зарубежных адресов. Это снижает стоимость запросов на 40% относительно использования только Google. DaData в 2 раза точнее Nominatim для адресов РФ — это критично для логистики. Google Places с session token в 3-5 раз дешевле, чем без токена.
Почему важен session token в Google Places?
На iOS используем GooglePlaces pod и GMSPlacesClient.findAutocompletePredictions(fromQuery:filter:sessionToken:callback:). Ключевой момент — GMSAutocompleteSessionToken: один токен на всю сессию поиска (от первого символа до выбора результата). Это снижает стоимость в 3–5 раз по сравнению с запросом без токена. Как говорит Google Places documentation: «Использование сессионных токенов позволяет сгруппировать несколько запросов в один биллинг».
let token = GMSAutocompleteSessionToken() let filter = GMSAutocompleteFilter() filter.type = .address filter.countries = ["RU", "BY", "KZ"] placesClient.findAutocompletePredictions( fromQuery: query, filter: filter, sessionToken: token ) { results, error in guard let results else { return } self.suggestions = results.map { $0.attributedFullText.string } } После выбора адреса вызываем fetchPlace(fromPlaceID:placeFields:sessionToken:) для получения координат — и обнуляем токен. Без fetchPlace координаты не получить через автодополнение.
На Android — Places.initialize(context, apiKey) + PlacesClient. В Jetpack Compose:
val placesClient = Places.createClient(context) val request = FindAutocompletePredictionsRequest.builder() .setQuery(query) .setSessionToken(AutocompleteSessionToken.newInstance()) .setTypesFilter(listOf(PlaceTypes.ADDRESS)) .setCountries("RU", "BY") .build() placesClient.findAutocompletePredictions(request) .addOnSuccessListener { response -> _suggestions.value = response.autocompletePredictions } Что даёт дебаунс запросов?
Без дебаунса каждое нажатие клавиши — отдельный API-запрос. При среднем вводе 4 символа в секунду это 4 запроса вместо одного. Наш опыт показывает, что правильный дебаунс 350 мс сокращает число запросов на 70%.
Пошаговая реализация дебаунса:
- Создайте Publisher/Flow из текстового поля.
- Примените оператор
debounce(for: 350 ms). - Добавьте фильтр длины запроса >= 3 символа.
- Используйте
flatMapLatestдля отмены предыдущего запроса.
На iOS через Combine:
searchTextField.textPublisher .debounce(for: .milliseconds(350), scheduler: DispatchQueue.main) .removeDuplicates() .sink { [weak self] query in guard query.count >= 3 else { return } self?.fetchSuggestions(for: query) } На Android через StateFlow:
searchQuery .debounce(350) .filter { it.length >= 3 } .distinctUntilChanged() .flatMapLatest { fetchSuggestions(it) } .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) flatMapLatest отменяет предыдущий запрос при новом вводе — без этого старые результаты могут перекрыть актуальные.
Офлайн и кэш: как улучшить UX?
Последние 10–20 выбранных адресов храним локально (UserDefaults / SharedPreferences) и показываем при пустом поле ввода. Это решает самый частый кейс: пользователь каждый раз заказывает домой.
Для истории поиска — Room / Core Data с колонками address_string, lat, lon, last_used_at. При вводе сначала ищем по локальной базе (LIKE-запрос), потом параллельно запрашиваем API — показываем сначала локальный результат, заменяем на API-результат при приходе. Время отклика из кэша — 2-5 мс против 200-500 мс из API.
Пример полного решения для iOS (SwiftUI + Combine)
class AddressSearchViewModel: ObservableObject { @Published var query = "" @Published var suggestions: [String] = [] private var cancellables = Set<AnyCancellable>() private let placesClient = GMSPlacesClient() private let token = GMSAutocompleteSessionToken() init() { $query .debounce(for: .milliseconds(350), scheduler: DispatchQueue.main) .removeDuplicates() .filter { $0.count >= 3 } .flatMapLatest { [weak self] query -> AnyPublisher<[String], Never> in guard let self = self else { return Just([]).eraseToAnyPublisher() } return Future { promise in let filter = GMSAutocompleteFilter() filter.type = .address filter.countries = ["RU"] self.placesClient.findAutocompletePredictions( fromQuery: query, filter: filter, sessionToken: self.token ) { results, error in guard let results = results, error == nil else { promise(.success([])) return } promise(.success(results.map { $0.attributedFullText.string })) } }.eraseToAnyPublisher() } .receive(on: DispatchQueue.main) .assign(to: &$suggestions) } func selectAddress(_ placeID: String) { let token = GMSAutocompleteSessionToken() let fields: GMSPlaceField = [.coordinate, .formattedAddress] placesClient.fetchPlace(fromPlaceID: placeID, placeFields: fields, sessionToken: token) { place, error in guard let coordinate = place?.coordinate else { return } // сохраняем координаты } } } Как тестировать автокомплит на краевых случаях?
Тестирование автокомплита выявляет ошибки, которые сложно отловить в production. Мы используем подход mock-слой: подменяем API-ответы тестовыми данными (пустой ответ, задержки, ошибки). Проверяем граничные строки: пустая строка, один символ, спецсимволы (!@#$), очень длинные адреса (200+ символов), адреса с нестандартными буквами (умлауты, кириллица). Также имитируем сбои сети и тайм-ауты — приложение должно корректно показывать fallback-сообщение и не крашиться. У нас есть чек-лист из 25+ сценариев для каждого проекта.
Что входит в работу?
- Выбор и интеграция провайдера (DaData, Google, Yandex).
- Разработка UI-компонента с выпадающим списком (SwiftUI / Jetpack Compose).
- Реализация дебаунса, отмены запросов, session token.
- Локальное кэширование истории адресов.
- Обработка ошибок: отсутствие сети, лимиты API, некорректный ввод.
- Тестирование на граничных строках (пустой ввод, спецсимволы, очень длинные адреса).
- Документация по интеграции.
Мы реализовали поиск адресов в 30+ проектах — от служб доставки до геоинформационных систем. Гарантируем стабильную работу при высоких нагрузках. Оцените свой проект — свяжитесь с нами для консультации.
Срок реализации: два–четыре дня — провайдер, UI, дебаунс, кэш истории, тестирование. Стоимость рассчитывается индивидуально под ваши задачи. Получите консультацию — узнайте, сколько займёт интеграция вашего приложения.







