Реалізація пошуку по адресі з підсказками у мобільному додатку
Поле введення адреси з підсказками — один із найконверсійніших UI-елементів у додатках доставки та логістики. Користувач вводить «Тверськ» і за 300 мілісекунд бачить список варіантів. Технічно за цим стоїть вибір провайдера, дебаунс, кешування сесій і правильна обробка вибору.
Провайдери і коли що обирати
| Провайдер | Сильні сторони | Слабі сторони |
|---|---|---|
| Google Places Autocomplete API | Найкраще покриття глобально, POI, бізнеси | Дорого при високому трафіку, слабше по корпусам в РФ |
| DaData | Найкращий по адресам Росії (ФІАС/КЛАДР) | Тільки РФ |
| Nominatim (OpenStreetMap) | Безплатно, глобально | Немає SLA, повільніше, гірше за якістю |
| HERE Geocoding | Добре в Європі, є офлайн-пакети | Дорожче за Google для малих обсягів |
| Яндекс Geocoder | Добре по СНД | Вимагає аккаунт, обмеження по умовам |
Для більшості російських проектів — комбо DaData + Google: DaData як перший пріоритет, Google як fallback для зарубіжних адрес.
Google Places SDK: правильна інтеграція
На iOS — GooglePlaces pod. Використовуємо GMSPlacesClient.findAutocompletePredictions(fromQuery:filter:sessionToken:callback:). Ключовий момент — GMSAutocompleteSessionToken: один токен на всю сесію пошуку (від першого символу до вибору результату). Це знижує вартість в 3-5 раз порівняно з запитом без токена.
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
}
Дебаунс і UX-деталі
Без дебаунса кожне нажаття клавіші — окремий API-запит. При середньому введенні 4 символи в секунду це 4 запити замість одного.
На 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 скасовує попередній запит при новому введенні — без цього старі результати можуть перекрити актуальні.
Офлайн і кеш
Останні 10-20 вибраних адрес зберігаємо локально (UserDefaults / SharedPreferences) і показуємо при пустому полі введення. Це вирішує найчастіший кейс: користувач кожен раз замовляє додому.
Для історії пошуку — Room / Core Data з колонками address_string, lat, lon, last_used_at. При введенні спочатку шукаємо по локальній базі (LIKE-запит), потім паралельно запитуємо API — показуємо спочатку локальний результат, заміняємо на API-результат при прибутті.
Строк реалізації: два-чотири дні — провайдер, UI-компонент, дебаунс, кеш історії, тестування на граничних строках.







