Search
Sourced from HERE's global data set of hundreds of millions of POIs and point addresses worldwide, the HERE SDK for Flutter 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 setting 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 that belong 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.
- Search offline: When no internet connection is available, you can switch to the
OfflineSearchEngine
to search on already cached map data or preloaded offline maps data.
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.
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 = SearchEngine();
} on InstantiationException {
throw Exception("Initialization of SearchEngine failed.");
}
Creating a new SearchEngine
instance can throw an InstantiationException
that we have to handle as shown above.
Search for Places
Let's assume we want to find all "pizza" places around the current map center shown on the device. Before we can start the search, we need to specify a few more details:
int maxItems = 30;
SearchOptions searchOptions = SearchOptions(LanguageCode.enUs, maxItems);
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 do a one-box search 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 = TextQuery.withBoxArea(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.
Preferably, 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.searchByText(query, searchOptions, (SearchError? searchError, List<Place>? list) async {
if (searchError != null) {
_showDialog("Search", "Error: " + searchError.toString());
return;
}
int listLength = list!.length;
_showDialog("Search for $queryString", "Results: $listLength. Tap marker to see details.");
for (Place searchResult in 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 never 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:
int listLength = list!.length;
_showDialog("Search for $queryString", "Results: $listLength. Tap marker to see details.");
for (Place searchResult in list) {
Metadata metadata = Metadata();
metadata.setCustomValue("key_search_result", SearchResultMetadata(searchResult));
addPoiMapMarker(searchResult.geoCoordinates!, metadata);
}
SearchResultMetadata
is a custom class that may look like below:
import 'package:here_sdk/core.dart';
import 'package:here_sdk/search.dart';
class SearchResultMetadata implements CustomMetadataValue {
Place searchResult;
SearchResultMetadata(Place place) : searchResult = place;
@override
String getTag() {
return "SearchResult Metadata";
}
@override
void release() {
}
}
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:
MapMarker topmostMapMarker = mapMarkerList.first;
Metadata? metadata = topmostMapMarker.metadata;
if (metadata != null) {
CustomMetadataValue? customMetadataValue = metadata.getCustomValue("key_search_result");
if (customMetadataValue != null) {
SearchResultMetadata searchResultMetadata = customMetadataValue as SearchResultMetadata;
String title = searchResultMetadata.searchResult.title;
String vicinity = searchResultMetadata.searchResult.address.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_app
example 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 make 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, 3rd level represents the sub category of the 2nd level sub-category. Each category level is defined as a number in the Places Category System.
As an example, we search below for all places that belong to the "Eat and Drink" category or to the "Shopping Electronics" category:
void searchForCategories() {
List<PlaceCategory> categoryList = [];
categoryList.add(PlaceCategory(PlaceCategory.eatAndDrink));
categoryList.add(PlaceCategory(PlaceCategory.shoppingElectronics));
CategoryQuery categoryQuery = CategoryQuery(categoryList, GeoCoordinates(52.520798, 13.409408));
int maxItems = 30;
SearchOptions searchOptions = SearchOptions(LanguageCode.enUs, maxItems);
_searchEngine.searchByCategory(categoryQuery, searchOptions, (SearchError? searchError, List<Place>? list) async {
if (searchError != null) {
return;
}
int listLength = list!.length;
for (Place searchResult in list) {
}
});
}
PlaceCategory
accepts a String
. Here we use the predefined categories eatAndDrink
and shoppingElectronics
. 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();
int maxItems = 5;
SearchOptions searchOptions = SearchOptions(LanguageCode.enUs, maxItems);
_searchEngine.suggest(
TextQuery.withAreaCenter(
"p",
centerGeoCoordinates),
searchOptions, (SearchError? searchError, List<Suggestion>? list) async {
_handleSuggestionResults(searchError, list);
});
_searchEngine.suggest(
TextQuery.withAreaCenter(
"pi",
centerGeoCoordinates),
searchOptions, (SearchError? searchError, List<Suggestion>? list) async {
_handleSuggestionResults(searchError, list);
});
_searchEngine.suggest(
TextQuery.withAreaCenter(
"piz",
centerGeoCoordinates),
searchOptions, (SearchError? searchError, List<Suggestion>? list) async {
_handleSuggestionResults(searchError, list);
});
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.
void _handleSuggestionResults(SearchError? searchError, List<Suggestion>? list) {
if (searchError != null) {
print("Autosuggest Error: " + searchError.toString());
return;
}
int listLength = list!.length;
print("Autosuggest results: $listLength.");
for (Suggestion autosuggestResult in list) {
String addressText = "Not a place.";
Place? place = autosuggestResult.place;
if (place != null) {
addressText = place.address.addressText;
}
print("Autosuggest result: " + autosuggestResult.title + " addressText: " + addressText);
}
}
Here we log 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 terms, 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
Now we have seen how to search for places at certain locations or areas on the map. But, what can we do if only a location is 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 interacted with the map. Although the user sees the location on the map, we don't know any other attributes 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 "geocode" that location, see the following method:
Future<void> _getAddressForCoordinates(GeoCoordinates geoCoordinates) async {
int maxItems = 1;
SearchOptions reverseGeocodingOptions = new SearchOptions(LanguageCode.enGb, maxItems);
_searchEngine.searchByCoordinates(geoCoordinates, reverseGeocodingOptions,
(SearchError? searchError, List<Place>? list) async {
if (searchError != null) {
_showDialog("Reverse geocoding", "Error: " + searchError.toString());
return;
}
_showDialog("Reverse geocoded address:", list!.first.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 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 never 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, 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 name or a city.
Note: Whereas reverse geocoding in most cases delivers only one result, geocoding may provide one or many 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 = AddressQuery.withAreaCenter(queryString, geoCoordinates);
int maxItems = 30;
SearchOptions searchOptions = SearchOptions(LanguageCode.deDe, maxItems);
_searchEngine.searchByAddress(query, searchOptions, (SearchError? searchError, List<Place>? list) async {
if (searchError != null) {
_showDialog("Geocoding", "Error: " + searchError.toString());
return;
}
String locationDetails = "";
for (Place geocodingResult in list!) {
}
int itemsCount = list!.length;
_showDialog("Geocoding result: $itemsCount", locationDetails);
});
For this example, we will pass in the street name of HERE's Berlin HQ "Invalidenstraße 116" - optionally followed by the city - as the query string. As this is a street name in German, we pass in the language code deDe
for Germany. 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 function 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 in list!) {
GeoCoordinates geoCoordinates = geocodingResult.geoCoordinates!;
Address address = geocodingResult.address;
locationDetails = address.addressText +
". GeoCoordinates: " +
geoCoordinates.latitude.toString() +
", " +
geoCoordinates.longitude.toString();
}
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.
The HERE SDK provides 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:
void _searchAlongARoute(here.Route route) {
int halfWidthInMeters = 200;
GeoCorridor routeCorridor = GeoCorridor.make(route.polyline, halfWidthInMeters);
TextQuery textQuery = TextQuery.withCorridorAreaAndAreaCenter(
"charging station", routeCorridor, _hereMapController.camera.state.targetCoordinates);
int maxItems = 30;
SearchOptions searchOptions = SearchOptions(LanguageCode.enUs, maxItems);
_searchEngine.searchByText(textQuery, searchOptions, (SearchError? searchError, List<Place>? items) {
if (searchError != null) {
if (searchError == SearchError.polylineTooLong) {
print("Search: Route too long or halfWidthInMeters too small.");
} else {
print("Search: No charging stations found along the route. Error: $searchError");
}
return;
}
print("Search: Search along route found ${items.length} charging stations:");
for (Place place in 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 there we simply rendered green circles to indicate start and destination of the route.
Especially for longer 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 ev_routing_app
example app on GitHub.
Offline Search
In addition to the SearchEngine
, there is also an equivalent for offline use cases available: The OfflineSearchEngine
. It can be constructed in the same way as its online counterpart we already showed above:
try {
_offlineSearchEngine = OfflineSearchEngine();
} on InstantiationException {
throw ("Initialization of OfflineSearchEngine failed.");
}
The OfflineSearchEngine
provides almost the same interfaces as the SearchEngine
, but the results may slightly differ as the results are taken from already downloaded or cached map data instead of initiating a new request to a HERE backend service.
This way the data may be, for example, older compared to the data you may receive when using the SearchEngine
. On the other hand, this class may provide results faster - as no online connection is necessary.
Note
You can only search on already cached or preloaded offline maps data. When you use only cached map data, it may happen that not all tiles are loaded. In that case, no results can be found until also these tiles are loaded. With offline maps this cannot happen and the required map data is guaranteed to be available for the downloaded region. Therefore, it is recommended to not rely on cached map data.
Although most of the available OfflineSearchEngine
interfaces are also available in the SearchEngine
, the opposite is not the case, simply because not every online feature is also accessible from offline data.
Note
Currently, it is not possible to use offline search in Japan and no results will be found.
Below we show one possible use case for the OfflineSearchEngine
. For example, when you are on the go, your connection can be temporarily lost. In such a case it makes sense to search in the already downloaded map data.
To do so, first you need to check if the device has lost its connectivity. As a second step, you can use the preferred search engine:
if (useOnlineSearchEngine) {
_onlineSearchEngine.searchByText(query, searchOptions, (SearchError? searchError, List<Place>? list) async {
_handleSearchResults(searchError, list, queryString);
});
} else {
_offlineSearchEngine.searchByText(query, searchOptions, (SearchError? searchError, List<Place>? list) async {
_handleSearchResults(searchError, list, queryString);
});
}
To handle the search results, you can use a _handleSearchResults()
function that is shown as part of the example app. The code to handle the results was already shown above.
In a similar fashion, you can also reverse geocode addresses:
if (useOnlineSearchEngine) {
_onlineSearchEngine.searchByCoordinates(geoCoordinates, reverseGeocodingOptions,
(SearchError? searchError, List<Place>? list) async {
_handleReverseGeocodingResults(searchError, list);
});
} else {
_offlineSearchEngine.searchByCoordinates(geoCoordinates, reverseGeocodingOptions,
(SearchError? searchError, List<Place>? list) async {
_handleReverseGeocodingResults(searchError, list);
});
}
Or you can geocode an address to a geographic coordinate with:
if (useOnlineSearchEngine) {
_onlineSearchEngine.searchByAddress(query, geocodingOptions, (SearchError? searchError, List<Place>? list) async {
_handleGeocodingResults(searchError, list, queryString);
});
} else {
_offlineSearchEngine.searchByAddress(query, geocodingOptions, (SearchError? searchError, List<Place>? list) async {
_handleGeocodingResults(searchError, list, queryString);
});
}
Note that the code to check if the device is online or not is left out here. You may use a 3rd party plugin for this or try to make an actual connection - and when that fails, you can switch to the OfflineSearchEngine
- or the other way round: You can try offline search first to provide a fast experience for the user, but when no map data is available, you can try online.
Note
You can find the full code for this section as part of the search_hybrid_app
example on GitHub.