# 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: ```java Java 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 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); } }; ``` ```kotlin Kotlin 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. > #### Note > > The reverse geocoding response contains either an error or a result: `SearchError` and 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. 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. > #### Note > > Whereas 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: ```java Java // 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 list) { if (searchError != null) { showDialog("Geocoding", "Error: " + searchError.toString()); return; } for (Place geocodingResult : list) { //... } showDialog("Geocoding result","Size: " + list.size()); } }; ``` ```kotlin Kotlin // 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. > #### Note > > Results 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. > #### Note > > If `searchError` is null, the resulting `list` is 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](https://www.here.com/docs/bundle/places-search-api-developer-guide/page/topics/what-is.html). Below, we iterate over the list and get the address text and the coordinates: ```java Java 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; //... } ``` ```kotlin Kotlin 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. ## 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. > #### Note > > The 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 `Suggestion` feature you get possible `Place` results based on a `TextQuery`: 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. ```java Java 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); ``` ```kotlin Kotlin 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. ```java Java private final SuggestCallback autosuggestCallback = new SuggestCallback() { @Override public void onSuggestCompleted(@Nullable SearchError searchError, @Nullable List 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); } } }; ``` ```kotlin Kotlin 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 the `SearchEngine` with an instance of the `OfflineSearchEngine`. 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](https://github.com/heremaps/here-sdk-examples/tree/master/examples/latest/navigate/android/Java/SearchHybrid) and [Kotlin](https://github.com/heremaps/here-sdk-examples/tree/master/examples/latest/navigate/android/Kotlin/SearchHybridKotlin) on [GitHub](https://github.com/heremaps/here-sdk-examples). ## 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](/geocoding-and-search/docs/result-types-place). 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: ```java private void searchForCategories() { List 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 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](/geocoding-and-search/docs/result-types-place). ```java PlaceChain placeChain = new PlaceChain("1566"); List 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](https://docs.here.com/here-sdk/docs/routing) 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: ```java 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: ```java // 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 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: ```java // 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 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. 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. 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. > #### Note > > You can find the full code for this section as part of the `EVRouting` example app on GitHub, in [Java](https://github.com/heremaps/here-sdk-examples/blob/master/examples/latest/navigate/android/Java/EVRouting) and [Kotlin](https://github.com/heremaps/here-sdk-examples/tree/master/examples/latest/navigate/android/Kotlin/EVRoutingKotlin).