Implementation of Address Search with Autocomplete in a Mobile Application
An address input field with suggestions is one of the most converting UI elements in delivery and logistics apps. User types "Tver" and sees a list of options in 300 milliseconds. Technically this involves choosing a provider, debounce, session caching, and proper selection handling.
Providers and When to Choose What
| Provider | Strengths | Weaknesses |
|---|---|---|
| Google Places Autocomplete API | Best global coverage, POI, businesses | Expensive with high traffic, weaker on buildings in RF |
| DaData | Best for Russian addresses (FIAS/KLADR) | Russia only |
| Nominatim (OpenStreetMap) | Free, global | No SLA, slower, lower quality |
| HERE Geocoding | Good in Europe, offline packages | More expensive than Google for small volumes |
| Yandex Geocoder | Good in CIS | Requires account, usage restrictions |
For most Russian projects — DaData + Google combo: DaData first priority, Google fallback for foreign addresses.
Google Places SDK: Correct Integration
On iOS — GooglePlaces pod. Use GMSPlacesClient.findAutocompletePredictions(fromQuery:filter:sessionToken:callback:). Key point — GMSAutocompleteSessionToken: one token per entire search session (from first character to selection). This reduces cost 3-5x compared to requests without token.
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 }
}
After selecting address, call fetchPlace(fromPlaceID:placeFields:sessionToken:) to get coordinates — and reset token. Without fetchPlace you can't get coordinates through autocomplete.
On Android — Places.initialize(context, apiKey) + PlacesClient. In 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
}
Debounce and UX Details
Without debounce, each keystroke is a separate API request. At average 4 characters per second, that's 4 requests instead of one.
On iOS via 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)
}
On Android via StateFlow:
searchQuery
.debounce(350)
.filter { it.length >= 3 }
.distinctUntilChanged()
.flatMapLatest { fetchSuggestions(it) }
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
flatMapLatest cancels previous request on new input — without this old results may overwrite current ones.
Offline and Cache
Store last 10-20 selected addresses locally (UserDefaults / SharedPreferences) and show when input is empty. This solves the most common case: user orders home every time.
For search history — Room / Core Data with columns address_string, lat, lon, last_used_at. On input, search local database first (LIKE query), then simultaneously request API — show local result first, replace with API result on arrival.
Timeline: two to four days — provider, UI component, debounce, cache history, testing on edge strings.







