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.
- Kotlin
- Java
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)
SearchPreferences preferences = new SearchPreferences();
preferences.setThresholdDistance(50);
Coordinates coordinates = new Coordinates(51.519305, -0.128022);
SearchService searchService = new SearchService(
preferences,
(results, errorCode, hint) -> {
if (errorCode == GemError.NoError) {
if (results.isEmpty()) {
Log.d("SearchService", "No results found");
} else {
Landmark landmark = results.get(0);
AddressInfo addressInfo = landmark.getAddressInfo();
String country = addressInfo != null ? addressInfo.getField(EAddressField.Country) : null;
String city = addressInfo != null ? addressInfo.getField(EAddressField.City) : null;
String street = addressInfo != null ? addressInfo.getField(EAddressField.StreetName) : null;
String streetNumber = addressInfo != null ? addressInfo.getField(EAddressField.StreetNumber) : null;
String fullAddress = addressInfo != null ? addressInfo.format() : null;
Log.d("SearchService", "Address: " + fullAddress);
}
} else {
Log.e("SearchService", "Error: " + GemError.getMessage(errorCode));
}
}
);
int 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.
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:
- Kotlin
- Java
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
GuidedAddressSearchService guidedSearchService = new GuidedAddressSearchService(
(results, errorCode, hint) -> {
if (errorCode == GemError.NoError || errorCode == 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.
- Kotlin
- Java
// 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)
}
// Address search method using CompletableFuture
CompletableFuture<Landmark> searchAddress(
Landmark landmark,
EAddressDetailLevel detailLevel,
String text
) {
CompletableFuture<Landmark> future = new CompletableFuture<>();
GuidedAddressSearchService guidedSearchService = new GuidedAddressSearchService(
(results, errorCode, hint) -> {
if (errorCode == GemError.NoError || errorCode == GemError.ReducedResult) {
if (results.isEmpty()) {
future.complete(null);
} else {
future.complete(results.get(0));
}
} else {
future.complete(null);
}
}
);
guidedSearchService.search(landmark, text, detailLevel);
return future;
}
Using the function above, we can look for the children landmarks following the conditions:
- Kotlin
- Java
// 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}")
}
// Use CompletableFuture chaining
GuidedAddressSearchService guidedSearchService = new GuidedAddressSearchService();
Landmark countryLandmark = guidedSearchService.getCountryLevelItem("ESP");
if (countryLandmark == null) {
Log.e("GuidedSearch", "Country not found");
return;
}
Log.d("GuidedSearch", "Country: " + countryLandmark.getName());
// Use the address search to get a landmark for a city in Spain (e.g., Barcelona).
searchAddress(countryLandmark, EAddressDetailLevel.City, "Barcelona")
.thenAccept(cityLandmark -> {
if (cityLandmark == null) {
Log.e("GuidedSearch", "City not found");
return;
}
Log.d("GuidedSearch", "City: " + cityLandmark.getName());
// Use the address search to get a predefined street's landmark in the city (e.g., Carrer de Mallorca).
searchAddress(cityLandmark, EAddressDetailLevel.Street, "Carrer de Mallorca")
.thenAccept(streetLandmark -> {
if (streetLandmark == null) {
Log.e("GuidedSearch", "Street not found");
return;
}
Log.d("GuidedSearch", "Street: " + streetLandmark.getName());
// Use the address search to get a predefined house number's landmark on the street (e.g., House Number 401).
searchAddress(streetLandmark, EAddressDetailLevel.HouseNumber, "401")
.thenAccept(houseNumberLandmark -> {
if (houseNumberLandmark == null) {
Log.e("GuidedSearch", "House number not found");
return;
}
Log.d("GuidedSearch", "House number: " + houseNumberLandmark.getName());
});
});
});
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.
- Kotlin
- Java
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")
}
ExternalInfo externalInfo = new ExternalInfo();
ProgressListener progressListener = new ProgressListener() {
@Override
public void notifyComplete(int errorCode, String hint) {
if (errorCode == GemError.NoError) {
String title = externalInfo.getWikiPageTitle();
String description = externalInfo.getWikiPageDescription();
String url = externalInfo.getWikiPageURL();
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:
- Kotlin
- Java
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
}
}
}
public class SearchActivity extends AppCompatActivity {
private SearchView searchView;
private SearchService currentSearchService;
private final Handler searchHandler = new Handler(Looper.getMainLooper());
private Runnable searchRunnable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
searchView = findViewById(R.id.search_view);
setupSearchView();
}
private void setupSearchView() {
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
// Handle search submission
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
// Cancel previous search and debounce new searches
if (searchRunnable != null) {
searchHandler.removeCallbacks(searchRunnable);
}
searchRunnable = () -> getAutoSuggestions(newText != null ? newText : "");
searchHandler.postDelayed(searchRunnable, 300); // 300ms debounce
return true;
}
});
}
private void getAutoSuggestions(String query) {
if (query.isEmpty()) return;
Log.d("AutoSuggestion", "New auto suggestion search for: " + query);
// Cancel previous search
if (currentSearchService != null) {
currentSearchService.cancelSearch();
}
Coordinates refCoordinates = new Coordinates(48.0, 2.0); // Or use current position
SearchPreferences preferences = new SearchPreferences();
preferences.setAllowFuzzyResults(true);
preferences.setMaxMatches(10);
currentSearchService = new SearchService(
preferences,
(results, errorCode, hint) -> {
if (errorCode == GemError.NoError) {
displaySuggestions(results);
Log.d("AutoSuggestion", "Got " + results.size() + " results for: " + query);
} else if (errorCode == 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 void displaySuggestions(LandmarkList landmarks) {
// Update your RecyclerView adapter or suggestion list here
// This is where you would populate your suggestions UI
for (Landmark landmark : landmarks) {
Log.d("Suggestion", "Landmark: " + landmark.getName());
// 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:
- Kotlin
- Java
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)
SearchService searchService = new SearchService(
(results, errorCode, hint) -> {
if (errorCode == GemError.NoError) {
if (results.isEmpty()) {
Log.d("RouteSearch", "No results");
} else {
Log.d("RouteSearch", "Results size: " + results.size());
for (Landmark landmark : results) {
// do something with landmarks
Log.d("RouteSearch", "Found: " + landmark.getName());
}
}
} else {
Log.e("RouteSearch", "No results found: " + GemError.getMessage(errorCode));
}
}
);
int 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.