GuidesFlutter API ReferencesHERE SDK for Android API referencesHERE SDK for iOS API references
Guides

Offline search features

Offline maps are only available with the Navigate license.

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:

do {
    // Allows to search on already downloaded or cached map data.
    try offlineSearchEngine = OfflineSearchEngine()
} catch let engineInstantiationError {
    fatalError("Failed to initialize OfflineSearchEngine. Cause: \(engineInstantiationError)")
}

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.

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 engine:

if isDeviceConnected {
    _ = searchEngine.searchByText(textQuery,
                            options: searchOptions,
                            completion: onSearchCompleted)
} else {
    _ = offlineSearchEngine.searchByText(textQuery,
                                   options: searchOptions,
                                   completion: onSearchCompleted)
}

To handle the search results, you can set the same onSearchCompleted handler (as already shown above).

In a similar fashion, you can also reverse geocode addresses:

if isDeviceConnected {
    _ = searchEngine.searchByCoordinates(geoCoordinates,
                            options: reverseGeocodingOptions,
                            completion: onReverseGeocodingCompleted)
} else {
    _ = offlineSearchEngine.searchByCoordinates(geoCoordinates,
                                   options: reverseGeocodingOptions,
                                   completion: onReverseGeocodingCompleted)
}

Or you can geocode an address to a geographic coordinate with:

if isDeviceConnected {
    _ = searchEngine.searchByAddress(query,
                            options: geocodingOptions,
                            completion: onGeocodingCompleted)
} else {
    _ = offlineSearchEngine.searchByAddress(query,
                                   options: geocodingOptions,
                                   completion: onGeocodingCompleted)
}

A possible solution to check the connectivity of your device can be implemented by trying to make an actual connection first - and when that fails, you can switch to the OfflineSearchEngine - or the other way round: you can try offline search first, 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 SearchHybrid example app 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.

Note

This 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.

func enableOfflineSearchIndexing(sdkNativeEngine: SDKNativeEngine) {
    var offlineSearchIndexOptions = OfflineSearchIndex.Options()
    offlineSearchIndexOptions.enabled = true

    offlineSearchIndexListener = OfflineSearchIndexListenerImpl()

    let error = OfflineSearchEngine.setIndexOptions(sdkEngine: sdkNativeEngine,
                                                    options: offlineSearchIndexOptions,
                                                    listener: offlineSearchIndexListener)
    if let error = error {
        print("Failed to set index options: \(error)")
        showMessage("Failed to set index options: \(error)")
    }
}

class OfflineSearchIndexListenerImpl: OfflineSearchIndexListener {

    func onStarted(operation: OfflineSearchIndex.Operation) {
        print("Indexing started. Operation: \(operation)")
    }

    func onProgress(percentage: Int32) {
        print("Indexing progress: \(percentage)%")
    }

    func onComplete(error: OfflineSearchIndex.Error?) {
        if let error = error {
            print("Indexing failed: \(error)")
        } else {
            print("Indexing completed successfully.")
        }
    }
}

Above, an OfflineSearchIndexListener is implemented to track the indexing process. This listener provides three callbacks to monitor the status of indexing. The onStarted() 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 onProgress() callback reports the current progress as a percentage to indicate the progress of index creation or deletion. Once indexing is completed, the onComplete() callback is invoked. If successful, error is nil. 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:

let geoPlaces = [
    GeoPlace.makeMyPlace(title: "Pizza Pino", coordinates: GeoCoordinates(latitude: 52.518032, longitude: 13.420632)),
    GeoPlace.makeMyPlace(title: "PVR mdh", coordinates: GeoCoordinates(latitude: 52.51772, longitude: 13.42038)),
    GeoPlace.makeMyPlace(title: "Harley's bar", coordinates: GeoCoordinates(latitude: 52.51764, longitude: 13.42062))
]

myPlaces.addPlaces(places: geoPlaces) { taskOutcome in
    if (taskOutcome == TaskOutcome.completed) {
      // Task completed.
    } else {
      // Task cancelled.
    }
}

This data source can then be attached to the OfflineSearchEngine:

offlineSearchEngine.attach(dataSource: myPlaces) { taskOutcome in
    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:

let queryArea = TextQuery.Area(inCircle: GeoCircle(center: GeoCoordinates(latitude: 52.518032, longitude: 13.420632),
                                                   radiusInMeters: 100.0))
let textQuery = TextQuery("Pizza Pino", area: queryArea)
offlineSearchEngine.searchByText(textQuery: textQuery, options: SearchOptions()) { (searchError, placeList) in
    // ...
}

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:

  • searchByCategory when CategoryQuery.Area.corridorArea is provided
  • searchByText when TextQuery.Area.corridorArea is provided

Note

The distributedResults feature is only available with the OfflineSearchEngine and requires offline map data to be available for the corridor area.

public func testDistributedResultsWithCorridor() {
    let categoryList = [PlaceCategory(id: PlaceCategory.eatAndDrink),
                        PlaceCategory(id: PlaceCategory.shoppingElectronics)]

    // Create a corridor area for the search - this is required for distributedResults to work
    let corridorPoints = [GeoCoordinates(latitude: 52.520798, longitude: 13.409408), // Start point (Berlin)
                          GeoCoordinates(latitude: 52.518032, longitude: 13.420632), // Middle point
                          GeoCoordinates(latitude: 52.515597, longitude: 13.377704), // End point
                          GeoCoordinates(latitude: 52.512000, longitude: 13.390000)] // Additional point for more complex corridor

    let queryArea = CategoryQuery.Area(
        inCorridor: GeoCorridor(polyline: corridorPoints),
        near: GeoCoordinates(latitude: 52.518032, longitude: 13.420632)
    )

    let categoryQuery = CategoryQuery(categoryList, area: queryArea)

    var searchOptions = SearchOptions(languageCode: LanguageCode.enUs, maxItems: 50, distributedResults: true)

    _ = offlineSearchEngine.searchByCategory(categoryQuery, options: searchOptions, completion: onDistributedSearchCompleted)
}

public func onDistributedSearchCompleted(error: SearchError?, items: [Place]?) {
    if let searchError = error {
        print("Search Error: \(searchError)")
        return
    }

    for (index, searchResult) in items!.enumerated() {
        let name = searchResult.title
        let address = searchResult.address.addressText
        let coords = searchResult.geoCoordinates

        print(String(
            format: "Result %d/%d: %@ at %@ (%.6f, %.6f)",
            index + 1, items!.count, name, address,
            coords!.latitude, coords!.longitude
        ))
    }
}