Search
Sourced from HERE's global data set of hundreds of millions of Points of Interests (POIs), streetnames and point addresses worldwide, the HERE SDK for Android makes it fast and easy to search. With the HERE SDK you can solve a variety of search related tasks from within a single SearchEngine
:
- Discover places: Search and discover places from HERE's vast database worldwide, either by category or by defining a search term.
- Generate auto suggestions: Search for places while typing a search term to offer query completion.
- Reverse geocode an address: Find the address that belongs to certain geographic coordinates.
- Geocode an address: Find the geographic coordinates correlated to an address.
- Search by ID: Search for a place identified by a HERE Place ID.
- Search along a route: Search for places along an entire route.
- Search by category along a route: Search for places based on categories along an entire route. This feature is in beta state.
One feature that all search variants have in common is that you can specify the location or area where you want to search. Setting an area can be done by passing in a rectangle area specified by a GeoBox
or even a circle area specified by a GeoCircle
. Any potential search result that lies outside the specified area is ranked with lower priority, except for relevant global results - for example, when searching for "Manhattan" in Berlin. The underlying search algorithms are optimized to help narrow down the list of results to provide faster and more meaningful results to the user.
Note: Each search request is performed asynchronously. An online connection is required when the SearchEngine is used to fetch results from HERE's backend services.
The massive database of places provided by the HERE Location Services can easily be discovered with the HERE SDK's SearchEngine
. Let's look at an example. We begin by creating a new SearchEngine
instance:
try {
searchEngine = new SearchEngine();
} catch (InstantiationErrorException e) {
throw new RuntimeException("Initialization of SearchEngine failed: " + e.error.name());
}
Creating a new SearchEngine
instance can throw an InstantiationErrorException
that we have to handle as shown above. Note that it is not possible to initialize an engine during the Application
's onCreate()
lifecycle. Any other point in time is fine. For example, a good place to initialize an engine may be in an Activity
's onCreate()
method.
Search for Places
Let's assume we want to find all "pizza" places around the current map center shown on the device. Before starting the search, we need to specify a few more details:
SearchOptions searchOptions = new SearchOptions();
searchOptions.languageCode = LanguageCode.EN_US;
searchOptions.maxItems = 30;
Here, we create a new SearchOptions
object holding the desired data:
- We can specify the language of the returned search results by setting a
LanguageCode
. -
maxItems
is set to define the maximum number of result items that should be delivered in the response.
In the example above, we limit the results to 30. If the engine discovers more search results than requested, it will return only the 30 most relevant search results.
We perform an area search (one-box) as we want to find all results within the current viewport. The SearchEngine
provides three different ways to specify the search location:
- Search at
GeoCoordinates
: Performs an asynchronous search request around the specified coordinates to provide the most relevant search results nearby. - Search in a
GeoCircle
area: Similar to the above, but searches for results within the specified circle area, which is defined by center geographic coordinates and a radius in meters. - Search in a
GeoBox
area: Similar to the above, but searches for results within the specified rectangle area, which is defined by the South West and North East coordinates passed as parameters.
You can specify the area and the term you want to search for together. For example, below you can set queryString
to "pizza":
GeoBox viewportGeoBox = getMapViewGeoBox();
TextQuery query = new TextQuery(queryString, viewportGeoBox);
Here we have left out the code for getMapViewGeoBox()
. You can create and pass in any GeoBox
that fits to your use case. A possible implementation can be found in the accompanying example apps.
Firstly, the results within the specified map area are returned. If no results were found, global search results may be returned. However, relevant global results such as prominent cities or states may be included - regardless of the specified search location.
Note: The query string can contain any textual description of the content you want to search for. You can pass in several search terms to narrow down the search results - with or without comma separation. So, "Pizza Chausseestraße" and "Pizza, Chausseestraße" will both lead to the same results and will find only pizza restaurants that lie on the street 'Chausseestraße'. Please also note that it is an error to pass in an empty query string, and in this case, the search will fail.
Finally, you can start to search asynchronously:
searchEngine.search(query, searchOptions, querySearchCallback);
...
private final SearchCallback querySearchCallback = new SearchCallback() {
@Override
public void onSearchCompleted(@Nullable SearchError searchError, @Nullable List<Place> list) {
if (searchError != null) {
showDialog("Search", "Error: " + searchError.toString());
return;
}
showDialog("Search", "Results: " + list.size());
for (Place searchResult : list) {
}
}
};
Before we can look into the results, first we should check for a possible SearchError
. For example, if the device is offline, the list
will be null and the error enum will indicate the cause. In this case, we call a helper method showDialog()
to show the error description to the user. A possible implementation of showDialog()
can be accessed from the accompanying "Search" example's source code - it does not contain any HERE SDK specific code.
Note
The search response contains either an error or a result: SearchError
and List<Place>
. Both can't be null at the same time - or non-null at the same time.
Now, it's time to look into the results. If no matching results could be found, an error would have been caught beforehand:
showDialog("Search", "Results: " + list.size());
for (Place searchResult : list) {
Metadata metadata = new Metadata();
metadata.setCustomValue("key_search_result", new SearchResultMetadata(searchResult));
addPoiMapMarker(searchResult.getGeoCoordinates(), metadata);
}
...
private static class SearchResultMetadata implements CustomMetadataValue {
public final Place searchResult;
public SearchResultMetadata(Place searchResult) {
this.searchResult = searchResult;
}
@NonNull
@Override
public String getTag() {
return "SearchResult Metadata";
}
}
Finally, we can iterate over the list of results. Each Place
contains various fields describing the found search result.
In our example, to add a marker to the map, we are interested in the place's location. In addition, we create a Metadata
object where we can store a SearchResult
.
Note: The Metadata
object can contain various data types to allow easy association of a MapMarker
with the result data. This way, we can hold all information related to a map marker in one object - this can be convenient when presenting this data, for example, after the user taps on a map marker. Even complex data objects can be stored by implementing the CustomMetadataValue
interface, as shown above.
A possible implementation of addPoiMapMarker()
can be accessed from the accompanying "Search" example's source code; see also the section about Map Markers in this guide. After you have at hand the picked map marker object, you can get the Metadata
information that we have set in the previous step:
Metadata metadata = topmostMapMarker.getMetadata();
if (metadata != null) {
CustomMetadataValue customMetadataValue = metadata.getCustomValue("key_search_result");
if (customMetadataValue != null) {
SearchResultMetadata searchResultMetadata = (SearchResultMetadata) customMetadataValue;
String title = searchResultMetadata.searchResult.getTitle();
String vicinity = searchResultMetadata.searchResult.getAddress().addressText;
showDialog("Picked Search Result",title + ". Vicinity: " + vicinity);
return;
}
}
Not all map markers may contain Metadata
. Unless you have set the Metadata
beforehand, getMetadata()
will return null. In this example, we simply check if the data stored for "key_search_result"
is not null, so that we know it must contain search data. We can then downcast to our custom type SearchResultMetadata
which holds the desired Place
.
Consult the API Reference for a complete overview on the available nullable fields.
Screenshot: Showing a picked search result with title and vicinity.
Note
You can find the full code for this and the following sections as part of the Search
example app on GitHub.
Search for Places Categories
Instead of doing a keyword search using TextQuery
as shown above, 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 1st, 2nd and 3rd level categories. While 1st level represents the main category, 2nd and 3rd 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 categoryQuery = new CategoryQuery(categoryList, new GeoCoordinates(52.520798, 13.409408));
SearchOptions searchOptions = new SearchOptions();
searchOptions.languageCode = LanguageCode.EN_US;
searchOptions.maxItems = 30;
searchEngine.search(categoryQuery, searchOptions, new SearchCallback() {
@Override
public void onSearchCompleted(SearchError searchError, List<Place> list) {
if (searchError != null) {
infoTextview.setText("Search Error: " + searchError.toString());
return;
}
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 overloaded search()
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 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.
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;
searchEngine.suggest(
new TextQuery("p",
centerGeoCoordinates),
searchOptions,
autosuggestCallback);
searchEngine.suggest(
new TextQuery("pi",
centerGeoCoordinates),
searchOptions,
autosuggestCallback);
searchEngine.suggest(
new TextQuery("piz",
centerGeoCoordinates),
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 suggest()
-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;
}
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);
}
}
};
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.
Reverse Geocode an Address from Geographic Coordinates
So far we have seen how to search for places at certain locations or areas on the map. But, what can we do if only the geographic coordinates are known? The most common use case for this might be a user who is doing some actions on the map. For example, a long press gesture. This will provide us with the latitude and longitude coordinates of the location where the user interacts with the map. Although the user sees the location on the map, we don't know any other attribute like the address information belonging to that location.
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.search(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;
}
showDialog("Reverse geocoded address:", list.get(0).getAddress().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 search()
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.
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.
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:
AddressQuery query = new AddressQuery(queryString, geoCoordinates);
SearchOptions options = new SearchOptions();
options.languageCode = LanguageCode.DE_DE;
options.maxItems = 30;
searchEngine.search(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());
}
};
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. Below, we iterate over the list and get the address text and the coordinates:
for (Place geocodingResult : list) {
GeoCoordinates geoCoordinates = geocodingResult.getGeoCoordinates();
Address address = geocodingResult.getAddress();
String 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.
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:
private void searchAlongARoute(Route route) {
int halfWidthInMeters = 200;
GeoCorridor routeCorridor = new GeoCorridor(route.getGeometry().vertices, halfWidthInMeters);
TextQuery textQuery = new TextQuery("charging station", routeCorridor,
mapView.getCamera().getState().targetCoordinates);
SearchOptions searchOptions = new SearchOptions();
searchOptions.languageCode = LanguageCode.EN_US;
searchOptions.maxItems = 30;
searchEngine.search(textQuery, searchOptions, new SearchCallback() {
@Override
public void onSearchCompleted(SearchError searchError, List<Place> items) {
if (searchError != null) {
if (searchError == SearchError.POLYLINE_TOO_LONG) {
Log.d("Search", "Route too long or halfWidthInMeters too small.");
} else {
Log.d("Search", "No charging stations found along the route. Error: " + searchError);
}
return;
}
Log.d("Search","Search along route found " + items.size() + " charging stations:");
for (Place place : items) {
}
}
});
}
As you can see, the GeoCorridor
requires the route's GeoPolyline
and a halfWidthInMeters. 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.
Especially for long routes, internally the search algorithm will try to optimize the search corridor. However, it may happen that a polyline is too long. As shown in the code snippet above, you can catch this case and then eventually decide to retrigger a search for a less complex route: This can be controlled by the halfWidthInMeters
parameter - a larger value will decrease the complexity of the corridor and therefore allow less precise results, but at least you will find more results this way.
Note that the complexity of a route is determined by several factors under the hood, so no definite length for a route can be given in general.
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.