Skip to main content

Search & Geocoding features

|

The Maps SDK for Android provides geocoding and reverse geocoding capabilities. Key features include:

  • Reverse Geocoding: Transform geographic coordinates into comprehensive address details, such as country, city, street name, postal code, and more.
  • Geocoding: Locate specific places (e.g., cities, streets, or house numbers) based on address components.
  • Route-Based Search: Perform searches along predefined routes to identify landmarks and points of interest.
  • Wikipedia Integration: Access Wikipedia descriptions and related information for identified landmarks.
  • Auto-Suggestion Implementation: Dynamically generate search suggestions for Android UI components.

Reverse geocode coordinates to address

Given a coordinate, we can get the corresponding address by searching around the given location and getting the AddressInfo associated with the nearest Landmark found nearby. The AddressInfo provides information about the country, city, street name, street number, postal code, state, district, country code, and other relevant information.

Fields from an AddressInfo object can be accessed via the getField method or can be automatically converted to a string containing the address info using the format method.

val preferences = SearchPreferences().apply {
thresholdDistance = 50
}
val coordinates = Coordinates(51.519305, -0.128022)

val searchService = SearchService(
preferences = preferences,
onCompleted = { results, errorCode, hint ->
when (errorCode) {
GemError.NoError -> {
if (results.isEmpty()) {
Log.d("SearchService", "No results found")
} else {
val landmark = results.first()
val addressInfo = landmark.addressInfo

val country = addressInfo?.getField(EAddressField.Country)
val city = addressInfo?.getField(EAddressField.City)
val street = addressInfo?.getField(EAddressField.StreetName)
val streetNumber = addressInfo?.getField(EAddressField.StreetNumber)

val fullAddress = addressInfo?.format()
Log.d("SearchService", "Address: $fullAddress")
}
}
else -> {
Log.e("SearchService", "Error: ${GemError.getMessage(errorCode)}")
}
}
}
)

val result = searchService.searchAroundPosition(coordinates)

Geocode address to location

The Maps SDK for Android provides geocoding capabilities to convert addresses into geographic coordinates. Addresses represent a tree-like structure, where each node is a Landmark with a specific EAddressDetailLevel. At the root of the tree, we have the country-level landmarks, followed by other levels such as cities, streets, and house numbers.

danger

The address structure is not the same in all countries. For example, some countries do not have states or provinces.

Use the getNextAddressDetailLevel method from the GuidedAddressSearchService class to get the next available levels in the address hierarchy.

Search countries by name

It is possible to do a search at a country level. In this case you can use code like the following:

val guidedSearchService = GuidedAddressSearchService(
onCompleted = { results, errorCode, hint ->
when (errorCode) {
GemError.NoError, GemError.ReducedResult -> {
if (results.isEmpty()) {
Log.d("GuidedSearch", "No results")
} else {
// do something with results
Log.d("GuidedSearch", "Found ${results.size} countries")
}
}
else -> {
Log.e("GuidedSearch", "Error: ${GemError.getMessage(errorCode)}")
}
}
}
)

// Search for countries by name - this will be handled by creating a search method
// that uses the general search functionality for country-level results

This can provide the parent landmark for the other GuidedAddressSearchService methods.

It does a search restricted to country-level results. This allows the use of more flexible search terms as it works regardless of the language.

Search the hierarchical address structure

We will create the example in two steps. First, we create a function that for a parent landmark, an EAddressDetailLevel and a text returns the children having the required detail level and matching the text.

The possible values for EAddressDetailLevel are: NoDetail, Country, State, County, District, City, Settlement, PostalCode, Street, StreetSection, StreetLane, StreetAlley, HouseNumber, Crossing.

// Address search method using coroutines
suspend fun searchAddress(
landmark: Landmark,
detailLevel: EAddressDetailLevel,
text: String
): Landmark? = suspendCoroutine { continuation ->
val guidedSearchService = GuidedAddressSearchService(
onCompleted = { results, errorCode, hint ->
when (errorCode) {
GemError.NoError, GemError.ReducedResult -> {
if (results.isEmpty()) {
continuation.resume(null)
} else {
continuation.resume(results.first())
}
}
else -> {
continuation.resume(null)
}
}
}
)

guidedSearchService.search(landmark, text, detailLevel)
}

Using the function above, we can look for the children landmarks following the conditions:

// Use coroutines in a lifecycle-aware scope
lifecycleScope.launch {
val guidedSearchService = GuidedAddressSearchService()
val countryLandmark = guidedSearchService.getCountryLevelItem("ESP")

if (countryLandmark == null) {
Log.e("GuidedSearch", "Country not found")
return@launch
}

Log.d("GuidedSearch", "Country: ${countryLandmark.name}")

// Use the address search to get a landmark for a city in Spain (e.g., Barcelona).
val cityLandmark = searchAddress(
landmark = countryLandmark,
detailLevel = EAddressDetailLevel.City,
text = "Barcelona"
)

if (cityLandmark == null) {
Log.e("GuidedSearch", "City not found")
return@launch
}

Log.d("GuidedSearch", "City: ${cityLandmark.name}")

// Use the address search to get a predefined street's landmark in the city (e.g., Carrer de Mallorca).
val streetLandmark = searchAddress(
landmark = cityLandmark,
detailLevel = EAddressDetailLevel.Street,
text = "Carrer de Mallorca"
)

if (streetLandmark == null) {
Log.e("GuidedSearch", "Street not found")
return@launch
}

Log.d("GuidedSearch", "Street: ${streetLandmark.name}")

// Use the address search to get a predefined house number's landmark on the street (e.g., House Number 401).
val houseNumberLandmark = searchAddress(
landmark = streetLandmark,
detailLevel = EAddressDetailLevel.HouseNumber,
text = "401"
)

if (houseNumberLandmark == null) {
Log.e("GuidedSearch", "House number not found")
return@launch
}

Log.d("GuidedSearch", "House number: ${houseNumberLandmark.name}")
}

The getCountryLevelItem method returns the root node corresponding to the specified country based on the provided country code. If the country code is invalid, the function will return null.

Geocode location to Wikipedia

A useful functionality when looking for something, is to obtain the content of a Wikipedia page for a specific text filter. This can be done by a normal search, followed by a call to ExternalInfo.requestWikiInfo.

val externalInfo = ExternalInfo()
val progressListener = object : ProgressListener() {
override fun notifyComplete(errorCode: ErrorCode, hint: String) {
when (errorCode) {
GemError.NoError -> {
val title = externalInfo.wikiPageTitle
val description = externalInfo.wikiPageDescription
val url = externalInfo.wikiPageURL

Log.d("Wikipedia", "Title: $title")
Log.d("Wikipedia", "Description: $description")
Log.d("Wikipedia", "URL: $url")
}
else -> {
Log.e("Wikipedia", "Error getting Wikipedia info: ${GemError.getMessage(errorCode)}")
}
}
}
}

// Check if landmark has Wikipedia info and request it
if (externalInfo.hasWikiInfo(landmark)) {
externalInfo.requestWikiInfo(landmark, progressListener)
} else {
Log.d("Wikipedia", "No Wikipedia info available for this landmark")
}

See the Location Wikipedia guide for more info.

Get auto-suggestions

Auto-suggestions can be implemented by calling SearchService.searchByFilter with the text filter set to the current text from the Android search widget when the field value is changed. A simple example is demonstrated below:

class SearchActivity : AppCompatActivity() {
private lateinit var searchView: SearchView
private var currentSearchService: SearchService? = null
private val searchHandler = Handler(Looper.getMainLooper())
private var searchRunnable: Runnable? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

searchView = findViewById(R.id.search_view)
setupSearchView()
}

private fun setupSearchView() {
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
// Handle search submission
return true
}

override fun onQueryTextChange(newText: String?): Boolean {
// Cancel previous search and debounce new searches
searchRunnable?.let { searchHandler.removeCallbacks(it) }

searchRunnable = Runnable {
getAutoSuggestions(newText ?: "")
}

searchHandler.postDelayed(searchRunnable!!, 300) // 300ms debounce
return true
}
})
}

private fun getAutoSuggestions(query: String) {
if (query.isEmpty()) return

Log.d("AutoSuggestion", "New auto suggestion search for: $query")

// Cancel previous search
currentSearchService?.cancelSearch()

val refCoordinates = Coordinates(48.0, 2.0) // Or use current position
val preferences = SearchPreferences().apply {
allowFuzzyResults = true
maxMatches = 10
}

currentSearchService = SearchService(
preferences = preferences,
onCompleted = { results, errorCode, hint ->
when (errorCode) {
GemError.NoError -> {
displaySuggestions(results)
Log.d("AutoSuggestion", "Got ${results.size} results for: $query")
}
GemError.Cancel -> {
Log.d("AutoSuggestion", "Search cancelled for: $query")
}
else -> {
Log.e("AutoSuggestion", "Search error for $query: ${GemError.getMessage(errorCode)}")
}
}
}
)

currentSearchService?.searchByFilter(query, refCoordinates)
}

private fun displaySuggestions(landmarks: LandmarkList) {
// Update your RecyclerView adapter or suggestion list here
// This is where you would populate your suggestions UI
for (landmark in landmarks) {
Log.d("Suggestion", "Landmark: ${landmark.name}")
// Add to suggestions adapter
}
}
}

The preferences with allowFuzzyResults set to true allows for partial match during search. The refCoordinates might be replaced with a more suitable value, such as the user current position or the map viewport center.

Search along a route

It is possible also to do a search along a route, not based on some coordinates. In this case you can use code like the following:

val searchService = SearchService(
onCompleted = { results, errorCode, hint ->
when (errorCode) {
GemError.NoError -> {
if (results.isEmpty()) {
Log.d("RouteSearch", "No results")
} else {
Log.d("RouteSearch", "Results size: ${results.size}")
for (landmark in results) {
// do something with landmarks
Log.d("RouteSearch", "Found: ${landmark.name}")
}
}
}
else -> {
Log.e("RouteSearch", "No results found: ${GemError.getMessage(errorCode)}")
}
}
}
)

val result = searchService.searchAlongRoute(route)

We can set a custom value for SearchPreferences.thresholdDistance in order to specify the maximum distance to the route for the landmarks to be searched. Other SearchPreferences fields can be specified depending on the usecase.