Оптимизация ввода адреса: автодополнение и кэширование
Представьте: пользователь вводит «Тверск», система за 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, дебаунс, кэш истории, тестирование. Стоимость рассчитывается индивидуально под ваши задачи. Получите консультацию — узнайте, сколько займёт интеграция вашего приложения.







