Offline search features
The ability to search for places efficiently is crucial, whether one is connected to the internet or working in an offline environment. To address both scenarios, the HERE SDK offers two versatile search solutions: the online-only SearchEngine and its offline equivalent, the OfflineSearchEngine. The OfflineSearchEngine is designed to provide the same robust search capabilities as its online counterpart, ensuring seamless information retrieval even without an internet connection. Just like the SearchEngine, the OfflineSearchEngine can be easily constructed and integrated into your applications:
try {
// Allows to search on already downloaded or cached map data.
_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.
NoteYou 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.
In addition, the place ID that identifies the location in the HERE Places API is different for offline search results.
Usually, with a free-form TextQuery, places can be found offline at least within a radius of 62.5 Km. Capitals can be found globally from anywhere. Adding a dedicated location such as a city name to the query string will search nearby that location.
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 third-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.
NoteYou can find the full code for this section as part of the "search_hybrid_app" example on GitHub.
Use offline search with indexing
In order to improve search results for the OfflineSearchEngine you can set OfflineSearchIndex.Options. By default, indexing is disabled.
Indexing provides a mechanism to find places in installed Region data - even if they are far away from the provided search center. This will help to match the behavior of the SearchEngine which does this, by default, without the need to create a search index as it operates only online.
You can set a OfflineSearchIndexListener to track the index building process.
NoteThis is a beta release of this feature, so there could be a few bugs and unexpected behaviors. APIs may change for new releases without a deprecation process.
Internally, indexing creates additional data on the device to make all installed map content searchable.
If indexing is enabled it will affect all operations that modify persistent storage content:
mapDownloader.downloadRegions(..)mapDownloader.deleteRegions(..)mapDownloader.clearPersistentMapStorage(..)mapDownloader.repairPersistentMap(..)mapUpdater.performMapUpdate(..)mapUpdater.updateCatalog(..)
These methods will ensure that the index is created, deleted or updated to contain entries from Region data that are installed in the persistent storage.
Creating an index takes time and this will make all operations that download or update offline maps longer, but usually not more than a few seconds up to a couple of minutes (depending on the amount of installed offline maps data). The stored index also increases the space occupied by offline maps by around 5%.
The _enableOfflineSearchIndexing(...) method demonstrates how to enable indexing for the OfflineSearchEngine and how to implement an IndexListener to monitor the indexing process.
_enableOfflineSearchIndexing(SDKNativeEngine sdkNativeEngine) {
var offlineSearchIndexOptions = OfflineSearchIndexOptions();
offlineSearchIndexOptions.enabled = true;
_offlineSearchIndexListener = OfflineSearchIndexListener(
//onStarted operation
(OfflineSearchIndexOperation operation) {
print("OfflineSearchIndexListener" + "Indexing started. Operation: " + operation.toString());
},
//onProgress operation
(int percentage) {
print("OfflineSearchIndexListener" + "Indexing progress: " + percentage.toString());
},
//onComplete operation
(OfflineSearchIndexError? error) {
if (error == null) {
print("OfflineSearchIndexListener" + "Indexing completed successfully");
} else {
print("OfflineSearchIndexListener" + "Indexing failed: " + error.name);
}
},
);
var offlineSearchIndexError = OfflineSearchEngine.setIndexOptions(sdkNativeEngine, offlineSearchIndexOptions, _offlineSearchIndexListener);
if (offlineSearchIndexError != null) {
print("Error occurred while enabling indexing:" + offlineSearchIndexError.name);
} else {
print("Indexing enabled successfully.");
}
}
Above, an OfflineSearchIndexListener is implemented to track the indexing process. This listener provides three callbacks to monitor the status of indexing. The _onStartedLambda() callback is called when indexing starts. This happens if map data changes or when OfflineSearchEngine.setIndexOptions() is called. No action is taken if the existing index matches the installed map regions. As indexing progresses, the _onProgressLambda() callback reports the current progress as a percentage to indicate the progress of index creation or deletion. Once indexing is completed, the _onCompleteLambda() callback is invoked. If successful, error is null. If it fails, the error provides details about the problem. Notifications are only sent if the index needs to be rebuilt.
Bring your own places for offline use
The HERE SDK allows to bring your own places at runtime when searching for places offline.
With the OfflineSearchEngine it is possible to inject custom data for places that can be found with the offline. Such personal places can be found by regular queries. The results are ranked as other places coming directly from HERE.
You can create multiple GeoPlace instances that can contain your custom place data. These instances can then be added to a MyPlaces data source:
List<GeoPlace> geoPlaces = [
GeoPlace.makeMyPlace("Pizza Pino", GeoCoordinates(52.518032, 13.420632)),
GeoPlace.makeMyPlace("PVR mdh", GeoCoordinates(52.51772, 13.42038)),
GeoPlace.makeMyPlace("Harley's bar", GeoCoordinates(52.51764, 13.42062))
];
_myPlaces.addPlaces(geoPlaces, (TaskOutcome taskOutcome) {
if (taskOutcome == TaskOutcome.completed) {
// Task completed.
} else {
// Task cancelled.
}
});
This data source can then be attached to the OfflineSearchEngine:
offlineSearchEngine.attach(_myPlaces, (TaskOutcome taskOutcome) {
if (taskOutcome == TaskOutcome.completed) {
// Task completed.
} else {
// Task cancelled.
}
});
addPlaces() and attach() operate asynchronously as thousands of places may be added at once by an application. Both methods return a TaskHandle, so that the operation can be cancelled. The TaskOutcomeevent informs when the operation has finished.
Below is one example how you can find such places:
double radiusInMeters = 100.0;
var queryArea = TextQueryArea.withCircle(GeoCircle(GeoCoordinates(52.518032, 13.420632), radiusInMeters));
TextQuery textQuery = TextQuery.withArea("Pizza Pino", queryArea);
offlineSearchEngine.searchByText(textQuery, SearchOptions(), (searchError, placeList) {
// ...
});
Note that the added places stay in memory as long as the offlineSearchEngine instance is alive.
Distributed results along corridors
The OfflineSearchEngine supports a search feature called distributedResults that ensures search results are well-distributed along a route corridor. This feature is particularly useful when you want to find points of interest evenly spaced along a travel route, rather than having all results clustered in one area. To use the distributedResults feature, you must set distributedResults = true in your SearchOptions and create a corridor area using GeoCorridor with multiple coordinate points. Note that this feature uses parallelization and is only supported for:
searchByCategorywhenCategoryQueryArea.corridorAreais providedsearchByTextwhenTextQueryArea.corridorAreais provided
NoteThe
distributedResultsfeature is only available with theOfflineSearchEngineand requires offline map data to be available for the corridor area.
void testDistributedResultsWithCorridor() {
// Create a corridor area for the search - this is required for distributedResults to work
List<GeoCoordinates> corridorPoints = [
new GeoCoordinates(52.520798, 13.409408), // Start point (Berlin)
new GeoCoordinates(52.518032, 13.420632), // Middle point
new GeoCoordinates(52.515597, 13.377704), // End point
new GeoCoordinates(52.512000, 13.390000) // Additional point for more complex corridor
];
CategoryQueryArea queryArea = new CategoryQueryArea.withCorridorAndCenter(new GeoCorridor.withPolyline(corridorPoints), new GeoCoordinates(52.518032, 13.420632));
List<PlaceCategory> categoryList = [];
categoryList.add(new PlaceCategory(PlaceCategory.eatAndDrink));
categoryList.add(new PlaceCategory(PlaceCategory.shoppingElectronics));
CategoryQuery categoryQuery = new CategoryQuery.withCategoriesInArea(categoryList, queryArea);
SearchOptions searchOptions = new SearchOptions();
searchOptions.languageCode = LanguageCode.enUs;
searchOptions.maxItems = 50;
searchOptions.distributedResults = true;
_offlineSearchEngine.searchByCategory(categoryQuery, searchOptions, (SearchError? searchError, List<Place>? list) {
if (searchError != null) {
// Handle error.
return;
}
// If error is null, list is guaranteed to be not empty.
String numberOfResults = "Search results: ${list?.length}. See log for details.";
for (int i = 0; i < list!.length; i++) {
Place searchResult = list[i];
String name = searchResult.title;
String address = searchResult.address.addressText;
GeoCoordinates? coords = searchResult.geoCoordinates;
print(
"Result ${i + 1}/${list.length}: "
"${searchResult.title} at ${searchResult.address.addressText} "
"(${searchResult.geoCoordinates?.latitude}, ${searchResult.geoCoordinates?.longitude})"
);
}
});
}
Updated yesterday