Search & Geocoding features
The HERE SDK offers a comprehensive suite of search and geocoding features that allow you to interact with geographical data seamlessly. Whether you're looking to convert coordinates into human-readable addresses or find locations based on address input, the HERE SDK has you covered.
Reverse geocode an address from geographic coordinates
With the SearchEngine, searching for places at specific locations or areas on the map is straightforward. But what if we only have a location's coordinates? A common scenario involves a user interacting with the map, such as performing a long press gesture, which provides the latitude and longitude of the selected location. While the user can see this location on the map, we lack additional details like the corresponding address information.
This is where reverse geocoding can be helpful.
Our location of interest is represented by a GeoCoordinates instance, which we might get from a user tapping the map, for example. To demonstrate how to "reverse geocode" that location, see the following method:
private void getAddressForCoordinates(GeoCoordinates geoCoordinates) {
SearchOptions reverseGeocodingOptions = new SearchOptions();
reverseGeocodingOptions.languageCode = LanguageCode.EN_GB;
reverseGeocodingOptions.maxItems = 1;
searchEngine.searchByCoordinates(geoCoordinates, reverseGeocodingOptions, addressSearchCallback);
}
private final SearchCallback addressSearchCallback = new SearchCallback() {
@Override
public void onSearchCompleted(@Nullable SearchError searchError, @Nullable List<Place> list) {
if (searchError != null) {
showDialog("Reverse geocoding", "Error: " + searchError.toString());
return;
}
// If error is null, list is guaranteed to be not empty.
showDialog("Reverse geocoded address:", list.get(0).getAddress().addressText);
}
};private fun getAddressForCoordinates(geoCoordinates: GeoCoordinates) {
val reverseGeocodingOptions = SearchOptions()
reverseGeocodingOptions.languageCode = LanguageCode.EN_GB
reverseGeocodingOptions.maxItems = 1
searchEngine!!.searchByCoordinates(
geoCoordinates,
reverseGeocodingOptions,
addressSearchCallback
)
}
private val addressSearchCallback =
SearchCallback { searchError, list ->
if (searchError != null) {
showDialog("Reverse geocoding", "Error: $searchError")
return@SearchCallback
}
// If error is null, list is guaranteed to be not empty.
showDialog("Reverse geocoded address:", list!![0].address.addressText)
}Similar to the other search functionalities provided by the SearchEngine, a SearchOptions instance needs to be provided to set the desired LanguageCode. It determines the language of the resulting address. Then we can make a call to the engine's searchByAddress() method to search online for the address of the passed coordinates. In case of errors, such as when the device is offline, SearchError holds the error cause.
NoteThe reverse geocoding response contains either an error or a result:
SearchErrorand the result list can't be null at the same time - or non-null at the same time.
The Address object contained inside each Place instance is a data class that contains multiple String fields describing the address of the raw location, such as country, city, street name, and many more. Consult the API Reference for more details. If you are only interested in receiving a readable address representation, you can access addressText, as shown in the above example. This is a String containing the most relevant address details, including the place's title.

Screenshot: Showing a long press coordinate resolved to an address.
Reverse geocoding does not need a certain search area: you can resolve coordinates to an address worldwide.
Geocode an address to a location
While with reverse geocoding you can get an address from raw coordinates, forward geocoding does the opposite and lets you search for the raw coordinates and other location details by just passing in an address detail such as a street or a city name.
NoteWhereas reverse geocoding in most cases delivers only one result, geocoding may provide one or more results.
Here is how you can do it. First, we must specify the coordinates near to where we want to search and as queryString, we set the address for which we want to find the exact location:
// The geoCoordinates act as a reference location to prioritize the search results.
// This helps the `SearchEngine` return addresses that are more relevant and closer to the user’s
// current location instead of global or less relevant matches.
AddressQuery query = new AddressQuery(queryString, geoCoordinates);
SearchOptions options = new SearchOptions();
options.languageCode = LanguageCode.DE_DE;
options.maxItems = 30;
searchEngine.searchByAddress(query, options, geocodeAddressSearchCallback);
...
private final SearchCallback geocodeAddressSearchCallback = new SearchCallback() {
@Override
public void onSearchCompleted(SearchError searchError, List<Place> list) {
if (searchError != null) {
showDialog("Geocoding", "Error: " + searchError.toString());
return;
}
for (Place geocodingResult : list) {
//...
}
showDialog("Geocoding result","Size: " + list.size());
}
};// The geoCoordinates act as a reference location to prioritize the search results.
// This helps the `SearchEngine` return addresses that are more relevant and closer to the user’s
// current location instead of global or less relevant matches.
val query = AddressQuery(queryString, geoCoordinates)
val options = SearchOptions()
options.languageCode = LanguageCode.DE_DE
options.maxItems = 30
searchEngine!!.searchByAddress(query, options, geocodeAddressSearchCallback)
...
private val geocodeAddressSearchCallback =
SearchCallback { searchError, list ->
if (searchError != null) {
showDialog("Geocoding", "Error: $searchError")
return@SearchCallback
}
for (geocodingResult in list!!) {
// Note: getGeoCoordinates() may return null only for Suggestions.
val geoCoordinates = geocodingResult.geoCoordinates
val address = geocodingResult.address
val locationDetails = (address.addressText
+ ". GeoCoordinates: " + geoCoordinates!!.latitude
+ ", " + geoCoordinates.longitude)
Log.d(
LOG_TAG,
"GeocodingResult: $locationDetails"
)
addPoiMapMarker(geoCoordinates)
}
showDialog("Geocoding result", "Size: " + list.size)
}For this example, we will pass in the street name of HERE's Berlin HQ "Invalidenstraße 116" - optionally followed by the city name - as the query string. As this is a street name in German, we pass in the language code DE_DE for German. This also determines the language of the returned results.
NoteResults can lie far away from the specified location - although results nearer to the specified coordinates are ranked higher and are returned preferably.
After validating that the SearchCallback completed without an error, we check the list for Place elements.
NoteIf
searchErroris null, the resultinglistis guaranteed to be not null, and vice versa.
The results are wrapped in a Place object that contains the raw coordinates - as well as some other address details, such as an Address object and the place ID that identifies the location in the HERE Places API. Below, we iterate over the list and get the address text and the coordinates:
for (Place geocodingResult : list) {
// Note: getGeoCoordinates() may return null only for Suggestions.
GeoCoordinates geoCoordinates = geocodingResult.getGeoCoordinates();
Address address = geocodingResult.getAddress();
String locationDetails = address.addressText
+ ". GeoCoordinates: " + geoCoordinates.latitude
+ ", " + geoCoordinates.longitude;
//...
}for (geocodingResult in list!!) {
// Note: getGeoCoordinates() may return null only for Suggestions.
val geoCoordinates = geocodingResult.geoCoordinates
val address = geocodingResult.address
val locationDetails = (address.addressText
+ ". GeoCoordinates: " + geoCoordinates!!.latitude
+ ", " + geoCoordinates.longitude)
//...
}See the screenshot below for an example of how this might look if the user picks such a result from the map. If you are interested, have a look at the accompanying "Search" example app, that shows how to search for an address text and to place map marker(s) at the found location(s) on the map.

Screenshot: Showing a picked geocoding result.
Get auto-suggestions
Most often, applications that offer places search, allow users to type the desired search term into an editable text field component. While typing, it is usually convenient to get predictions for possible terms.
The suggestions provided by the engine are ranked to ensure that the most relevant terms appear top in the result list. For example, the first list item could be used to offer auto completion of the search term currently typed by the user. Or, you can display a list of possible matches that are updated while the user types. A user can then select from the list of suggestions a suitable keyword and either start a new search for the selected term - or you can already take the details of the result such as title and vicinity and present it to the user.
NoteThe HERE SDK does not provide any UI or a fully integrated auto completion solution. Such a solution can be implemented by an application, if desired. With the
Suggestionfeature you get possiblePlaceresults based on aTextQuery: from these places you can use the title text ("Pizza XL") or other relevant place information (such as addresses) to provide feedback to a user - for example, to propose a clickable completion result. However, such a solution depends on the individual requirements of an application and needs to be implemented on app side using platform APIs.
Compared to a normal text query, searching for suggestions is specialized in giving fast results, ranked by priority, for typed query terms.
Let's see how the engine can be used to search for suggestions.
GeoCoordinates centerGeoCoordinates = getMapViewCenter();
SearchOptions searchOptions = new SearchOptions();
searchOptions.languageCode = LanguageCode.EN_US;
searchOptions.maxItems = 5;
TextQuery.Area queryArea = new TextQuery.Area(centerGeoCoordinates);
// Simulate a user typing a search term.
searchEngine.suggestByText(
new TextQuery("p", // User typed "p".
queryArea),
searchOptions,
autosuggestCallback);
searchEngine.suggestByText(
new TextQuery("pi", // User typed "pi".
queryArea),
searchOptions,
autosuggestCallback);
searchEngine.suggestByText(
new TextQuery("piz", // User typed "piz".
queryArea),
searchOptions,
autosuggestCallback);val centerGeoCoordinates = mapViewCenter
val searchOptions = SearchOptions()
searchOptions.languageCode = LanguageCode.EN_US
searchOptions.maxItems = 5
val queryArea = TextQuery.Area(centerGeoCoordinates)
// Simulate a user typing a search term.
searchEngine!!.suggestByText(
TextQuery(
"p", // User typed "p".
queryArea
),
searchOptions,
autosuggestCallback
)
searchEngine!!.suggestByText(
TextQuery(
"pi", // User typed "pi".
queryArea
),
searchOptions,
autosuggestCallback
)
searchEngine!!.suggestByText(
TextQuery(
"piz", // User typed "piz".
queryArea
),
searchOptions,
autosuggestCallback
)The helper method getMapViewCenter() is left out here, you can find it in the accompanying example app. It simply returns the GeoCoordinates that are currently shown at the center of the map view.
For each new text input, we make a request: assuming the user plans to type "Pizza" - we are looking for the results for "p" first, then for "pi" and finally for "piz." If the user really wants to search for "Pizza," then there should be enough interesting suggestions for the third call.
Please note that the suggestByText()-method returns a TaskHandle that can be optionally used to check the status of an ongoing call - or to cancel a call.
Let's see how the results can be retrieved.
private final SuggestCallback autosuggestCallback = new SuggestCallback() {
@Override
public void onSuggestCompleted(@Nullable SearchError searchError, @Nullable List<Suggestion> list) {
if (searchError != null) {
Log.d(LOG_TAG, "Autosuggest Error: " + searchError.name());
return;
}
// If error is null, list is guaranteed to be not empty.
Log.d(LOG_TAG, "Autosuggest results: " + list.size());
for (Suggestion autosuggestResult : list) {
String addressText = "Not a place.";
Place place = autosuggestResult.getPlace();
if (place != null) {
addressText = place.getAddress().addressText;
}
Log.d(LOG_TAG, "Autosuggest result: " + autosuggestResult.getTitle() +
" addressText: " + addressText);
}
}
};private val autosuggestCallback =
SuggestCallback { searchError, list ->
if (searchError != null) {
Log.d(LOG_TAG, "Autosuggest Error: " + searchError.name)
return@SuggestCallback
}
// If error is null, list is guaranteed to be not empty.
Log.d(LOG_TAG, "Autosuggest results: " + list!!.size)
for (autosuggestResult in list) {
var addressText = "Not a place."
val place = autosuggestResult.place
if (place != null) {
addressText = place.address.addressText
}
Log.d(
LOG_TAG, "Autosuggest result: " + autosuggestResult.title +
" addressText: " + addressText
)
}
}Here we define a SuggestCallback which logs the list items found in Suggestion. If there is no error, the engine will guarantee a list of results - otherwise it will be null.
Not every suggestion is a place. For example, it can be just a generic term like 'disco' that you can feed into a new search. With generic keyword, the Suggestion result does not contain a Place object, but only a title - as it represents a text without referring to a specific place. Please refer to the API Reference for all available fields of a Suggestion result.
Note that while the results order is ranked, there is no guarantee of the order in which the callbacks arrive. So, in rare cases, you may receive the "piz" results before the "pi" results.
Note (only for Navigate)You can use suggestions also with the
OfflineSearchEngine. In this case, just swap theSearchEnginewith an instance of theOfflineSearchEngine. Note that this requires downloaded or cached map data. You can find an example for this in the "[SearchHybrid]" example app, provided in both Java and Kotlin on GitHub.
Search for places categories
Instead of doing a keyword search using TextQuery like "Pizza", you can also search for categories to limit the Place results to the expected categories.
Category IDs follow a specific format and there are more than 700 different categories available on the HERE platform. Luckily, the HERE SDK provides a set of predefined values to perform category search easier to use. If needed, you can also pass custom category strings following the format xxx-xxxx-xxxx, where each group stands for first, second and third level categories. While first level represents the main category, second and third level describe the sub categories organized by logical subsets. Each category level is defined as a number in the Places Category System. As example: 100 - 'Eat and Drink' main category In this category we find category '1000' - 'Restaurant' In this level 2 category we find category '0001' 'Casual Dining' the complete Category ID will be: 100-1000-0001
As an example, we search below for all places that belong to the "Eat and Drink" category or to the "Shopping Electronics" category:
private void searchForCategories() {
List<PlaceCategory> categoryList = new ArrayList<>();
categoryList.add(new PlaceCategory(PlaceCategory.EAT_AND_DRINK));
categoryList.add(new PlaceCategory(PlaceCategory.SHOPPING_ELECTRONICS));
CategoryQuery.Area queryArea = new CategoryQuery.Area(new GeoCoordinates(52.520798, 13.409408));
CategoryQuery categoryQuery = new CategoryQuery(categoryList, queryArea);
SearchOptions searchOptions = new SearchOptions();
searchOptions.languageCode = LanguageCode.EN_US;
searchOptions.maxItems = 30;
searchEngine.searchByCategory(categoryQuery, searchOptions, new SearchCallback() {
@Override
public void onSearchCompleted(SearchError searchError, List<Place> list) {
if (searchError != null) {
infoTextview.setText("Search Error: " + searchError.toString());
return;
}
// If error is null, list is guaranteed to be not empty.
String numberOfResults = "Search results: " + list.size() + ". See log for details.";
infoTextview.setText(numberOfResults);
for (Place searchResult : list) {
String addressText = searchResult.getAddress().addressText;
Log.d(TAG, addressText);
}
}
});
}PlaceCategory accepts a String. Here we use the predefined categories EAT_AND_DRINK and SHOPPING_ELECTRONICS. The String value contains the ID as represented in the Places Category System. Again, we use the searchByCategory() method of the SearchEngine and pass a CategoryQuery object that contains the category list and the geographic coordinates where we want to look for places.
Search for place chains
Place chain allows to search for all places that belong to a certain brand. For example, if the ID represents "K-Supermarket", then you can limit your search to find only "K-Supermarket" stores. Make sure that the PlaceCategory will match the place chain's category.
The PlaceChain identifier is used to associate a place chain. The list of supported place chains can be found at this link.
PlaceChain placeChain = new PlaceChain("1566");
List<PlaceChain> includeChains = new ArrayList<>();
includeChains.add(placeChain);
categoryQuery.includeChains = includeChains;The above chain ID "1566" identifies the "McDonald's" place chain, which falls under the EAT_AND_DRINK category. If you set multiple place categories, then at least one category needs to match or no results will be found.
Search along a route
The SearchEngine provides support for a special search case when you do not want to search in a rectangular or circle area, but instead along a more complex GeoCorridor that can be defined by a GeoPolyline and other parameters.
The most common scenario for such a case may be to search along a Route for restaurants. Let's assume you already calculated a Route object. See the Directions section to learn how to calculate a route. By specifying a TextQuery, you can then easily define a rectangular area that would encompass an entire route:
TextQuery textQuery = TextQuery("restaurants", route.getBoundingBox())However, for longer routes - and depending on the shape of the route - results may lie very far away from the actual route path - as the route.boundingBox needs to encompass the whole route in a rectangular area.
The HERE SDK provides a more accurate solution by providing a GeoCorridor class that allows to determine the search area from the actual shape of the route. This way, only search results that lie on or beneath the path are included.
Below you can see an example how to search for charging stations along a route:
// Perform a search for charging stations along the found route.
private void searchAlongARoute(Route route) {
// We specify here that we only want to include results
// within a max distance of xx meters from any point of the route.
int halfWidthInMeters = 200;
GeoCorridor routeCorridor = new GeoCorridor(route.getGeometry().vertices, halfWidthInMeters);
PlaceCategory placeCategory = new PlaceCategory(PlaceCategory.BUSINESS_AND_SERVICES_EV_CHARGING_STATION);
CategoryQuery.Area categoryQueryArea = new CategoryQuery.Area(routeCorridor, mapView.getCamera().getState().targetCoordinates);
CategoryQuery categoryQuery = new CategoryQuery(placeCategory, categoryQueryArea);
SearchOptions searchOptions = new SearchOptions();
searchOptions.languageCode = LanguageCode.EN_US;
searchOptions.maxItems = 30;
searchEngine.searchByCategory(categoryQuery, searchOptions, new SearchCallback() {
@Override
public void onSearchCompleted(SearchError searchError, List<Place> items) {
if (searchError != null) {
Log.d("Search", "No charging stations found along the route. Error: " + searchError);
return;
}
// If error is nil, it is guaranteed that the items will not be nil.
Log.d("Search","Search along route found " + items.size() + " charging stations:");
for (Place place : items) {
// ...
}
}
});
}Fetching availability status for EV charging stations
Availability information, including the total number of connectors, occupied and available connectors, and additional details. The enableEVChargingStationDetails() option enables online retrieval of EV charging station availability, including the status of individual charging stations.
Attempting to use this feature without enabled credentials will result in a SearchError. For more information, please refer to the API reference.
This feature requires a custom option call as shown below:
// Enable fetching online availability details for EV charging stations.
// It allows retrieving additional details, such as whether a charging station is currently occupied.
// Check the API Reference for more details.
private void enableEVChargingStationDetails() {
// Fetching additional charging stations details requires a custom option call.
SearchError error = searchEngine.setCustomOption("browse.show", "ev");
if (error != null) {
showDialog("Charging station",
"Failed to enableEVChargingStationDetails.");
} else {
Log.d("ChargingStation", "EV charging station availability enabled successfully.");
}
}
private void searchAlongARoute(Route route) {
// We specify here that we only want to include results
// within a max distance of xx meters from any point of the route.
int halfWidthInMeters = 200;
GeoCorridor routeCorridor = new GeoCorridor(route.getGeometry().vertices, halfWidthInMeters);
PlaceCategory placeCategory = new PlaceCategory(PlaceCategory.BUSINESS_AND_SERVICES_EV_CHARGING_STATION);
CategoryQuery.Area categoryQueryArea = new CategoryQuery.Area(routeCorridor, mapView.getCamera().getState().targetCoordinates);
CategoryQuery categoryQuery = new CategoryQuery(placeCategory, categoryQueryArea);
SearchOptions searchOptions = new SearchOptions();
searchOptions.languageCode = LanguageCode.EN_US;
searchOptions.maxItems = 30;
enableEVChargingStationDetails();
searchEngine.searchByCategory(categoryQuery, searchOptions, new SearchCallback() {
@Override
public void onSearchCompleted(SearchError searchError, List<Place> items) {
if (searchError != null) {
Log.d("Search", "No charging stations found along the route. Error: " + searchError);
return;
}
// If error is nil, it is guaranteed that the items will not be nil.
Log.d("Search","Search along route found " + items.size() + " charging stations:");
for (Place place : items) {
Details details = place.getDetails();
Metadata metadata = getMetadataForEVChargingPools(details);
boolean foundExistingChargingStation = false;
for (MapMarker mapMarker : mapMarkers) {
if (mapMarker.getMetadata() != null) {
String id = mapMarker.getMetadata().getString(REQUIRED_CHARGING_METADATA_KEY);
if (id != null && id.equalsIgnoreCase(place.getId())) {
Log.d("Search", "Insert metdata to existing charging station: This charging station was already required to reach the destination (see red charging icon).");
mapMarker.setMetadata(metadata);
foundExistingChargingStation = true;
break;
}
}
}
if (!foundExistingChargingStation) {
addMapMarker(place.getGeoCoordinates(), R.drawable.charging, metadata);
}
}
}
});
}
private Metadata getMetadataForEVChargingPools(Details placeDetails) {
Metadata metadata = new Metadata();
if (placeDetails.evChargingPool != null) {
for (EVChargingStation station : placeDetails.evChargingPool.chargingStations) {
if (station.supplierName != null) {
metadata.setString(SUPPLIER_NAME_METADATA_KEY, station.supplierName);
}
if (station.connectorCount != null) {
metadata.setString(CONNECTOR_COUNT_METADATA_KEY, String.valueOf(station.connectorCount));
}
if (station.availableConnectorCount != null) {
metadata.setString(AVAILABLE_CONNECTORS_METADATA_KEY, String.valueOf(station.availableConnectorCount));
}
if (station.occupiedConnectorCount != null) {
metadata.setString(OCCUPIED_CONNECTORS_METADATA_KEY, String.valueOf(station.occupiedConnectorCount));
}
if (station.outOfServiceConnectorCount != null) {
metadata.setString(OUT_OF_SERVICE_CONNECTORS_METADATA_KEY, String.valueOf(station.outOfServiceConnectorCount));
}
if (station.reservedConnectorCount != null) {
metadata.setString(RESERVED_CONNECTORS_METADATA_KEY, String.valueOf(station.reservedConnectorCount));
}
if (station.lastUpdated != null) {
metadata.setString(LAST_UPDATED_METADATA_KEY, String.valueOf(station.lastUpdated));
}
}
}
return metadata;
}See the screenshot below for an example of how this appears when you click on a charging station. A pop-up will display the charging station name, connector count, available connectors, last updated time, and more.

Screenshot: Showing charging station availability details.
As you can see, the GeoCorridor requires the route's GeoPolyline and a halfWidthInMeters parameter. This value defines the farthest edges from any point on the polyline to the edges of the corridor. With a small value, the resulting corridor will define a very close area along the actual route.

Screenshot: Showing found charging stations along a route.
At the start and destination coordinates of the route, the corridor will have a round shape - imagine a snake with a certain thickness, but just with round edges at head and tail. Do not confuse this with the shown screenshot above, as we simply rendered green circles to better indicate start and destination of the route.
For very long routes, internally the search algorithm will try to optimize the search corridor. This can be controlled also on app-side with the halfWidthInMeters parameter - a larger value will decrease the complexity of the corridor and therefore result in less precise results as a trade-off.
If no error occurred, you can handle the Place results as already shown in the sections above.
NoteYou can find the full code for this section as part of the
EVRoutingexample app on GitHub, in Java and Kotlin.
Updated yesterday