> ## Documentation Index
> Fetch the complete documentation index at: https://docs.here.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Offline search features

<style>
  {`
    .beta-banner {
      color: #000000;
      background: linear-gradient(270deg, #65EBE2 0%, #6B9CFF 100%);
      font-weight: bold;
      padding: 5px;
      font-family: FiraGo, sans-serif;
    }
  `}
</style>

<p className="beta-banner">Offline maps are only available with the Navigate license.</p>

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:

```java Java
try {
    // Allows to search on already downloaded or cached map data.
    offlineSearchEngine = new OfflineSearchEngine();
} catch (InstantiationErrorException e) {
    throw new RuntimeException("Initialization of OfflineSearchEngine failed: " + e.error.name());
}
```

```kotlin Kotlin
try {
    // Allows to search on already downloaded or cached map data.
    offlineSearchEngine = OfflineSearchEngine()
} catch (e: InstantiationErrorException) {
    throw RuntimeException("Initialization of OfflineSearchEngine failed: " + e.error.name)
}
```

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](https://docs.here.com/geocoding-and-search/docs/introduction-to-here-geocoding-search-api-v7) 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:

```java Java
private boolean isDeviceConnected() {
  // An application may define here a logic to determine whether a device is connected or not.
  return isDeviceConnected;
}

...

if (isDeviceConnected()) {
    searchEngine.searchByText(query, searchOptions, querySearchCallback);
} else {
    offlineSearchEngine.searchByText(query, searchOptions, querySearchCallback);
}
```

```kotlin Kotlin
// An application may define here a logic to determine whether a device is connected or not.
// For this example app, the flag is set from UI.
private var isDeviceConnected = true

...

if (isDeviceConnected) {
    searchEngine.searchByText(query, searchOptions, querySearchCallback)
} else {
    offlineSearchEngine.searchByText(query, searchOptions, querySearchCallback)
}
```

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

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

```java Java
if (isDeviceConnected()) {
    searchEngine.searchByCoordinates(geoCoordinates, reverseGeocodingOptions, addressSearchCallback);
} else {
    offlineSearchEngine.searchByCoordinates(geoCoordinates, reverseGeocodingOptions, addressSearchCallback);
}
```

```kotlin Kotlin
if (isDeviceConnected) {
    searchEngine.searchByCoordinates(
        geoCoordinates,
        reverseGeocodingOptions,
        addressSearchCallback
    )
} else {
    offlineSearchEngine.searchByCoordinates(
        geoCoordinates,
        reverseGeocodingOptions,
        addressSearchCallback
    )
}
```

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

```java Java
if (isDeviceConnected()) {
    searchEngine.searchByAddress(query, options, geocodeAddressSearchCallback);
} else {
    offlineSearchEngine.searchByAddress(query, options, geocodeAddressSearchCallback);
}
```

```kotlin Kotlin
if (isDeviceConnected) {
    searchEngine.searchByAddress(query, options, geocodeAddressSearchCallback)
} else {
    offlineSearchEngine.searchByAddress(query, options, geocodeAddressSearchCallback)
}
```

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](https://github.com/heremaps/here-sdk-examples/).

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

```java Java
private void enableOfflineSearchIndexing(SDKNativeEngine sdkNativeEngine) {
    offlineSearchIndexOptions = new OfflineSearchIndex.Options();
    offlineSearchIndexOptions.enabled = true;

    offlineSearchIndexListener = new OfflineSearchIndexListener() {
        @Override
        public void onStarted(@NonNull OfflineSearchIndex.Operation operation) {
            Log.d("OfflineSearchIndexListener", "Indexing started. Operation: " + operation);
            snackbar.setText("Indexing started: " + operation).show();
        }

        @Override
        public void onProgress(int percentage) {
            Log.d("OfflineSearchIndexListener", "Indexing progress: " + percentage + "%");
            snackbar.setText("Indexing progress: " + percentage + "%").show();
        }

        @Override
        public void onComplete(@Nullable OfflineSearchIndex.Error error) {
            if (error == null) {
                Log.d("OfflineSearchIndexListener", "Indexing completed successfully.");
                snackbar.setText("Indexing completed successfully.").show();
            } else {
                Log.e("OfflineSearchIndexListener", "Indexing failed: " + error.name());
                snackbar.setText("Indexing failed: " + error.name()).show();
            }
        }
    };

    offlineSearchEngine.setIndexOptions(sdkNativeEngine, offlineSearchIndexOptions, offlineSearchIndexListener);
}
```

```kotlin Kotlin
private fun enableOfflineSearchIndexing(sdkNativeEngine: SDKNativeEngine) {
    offlineSearchIndexOptions = OfflineSearchIndex.Options()
    offlineSearchIndexOptions!!.enabled = true

    offlineSearchIndexListener = object : OfflineSearchIndexListener {
        override fun onStarted(operation: OfflineSearchIndex.Operation) {
            Log.d(
                "OfflineSearchIndexListener",
                "Indexing started. Operation: $operation"
            )
            snackbar.show("Indexing started: $operation")
        }

        override fun onProgress(percentage: Int) {
            Log.d("OfflineSearchIndexListener", "Indexing progress: $percentage%")
            snackbar.show("Indexing progress: $percentage%")
        }

        override fun onComplete(error: OfflineSearchIndex.Error?) {
            if (error == null) {
                Log.d("OfflineSearchIndexListener", "Indexing completed successfully.")
                snackbar.show("Indexing completed successfully.")
            } else {
                Log.e("OfflineSearchIndexListener", "Indexing failed: " + error.name)
                snackbar.show("Indexing failed: " + error.name)
            }
        }
    }

    OfflineSearchEngine.setIndexOptions(
        sdkNativeEngine,
        offlineSearchIndexOptions, offlineSearchIndexListener
    )
}
```

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

```java
List<GeoPlace> geoPlaces = Arrays.asList (
          GeoPlace.makeMyPlace("Pizza Pino", new GeoCoordinates(52.518032, 13.420632)),
          GeoPlace.makeMyPlace("PVR mdh", new GeoCoordinates(52.51772, 13.42038)),
          GeoPlace.makeMyPlace("Harley's bar", new GeoCoordinates(52.51764, 13.42062))
  );

  myPlaces.addPlaces(geoPlaces, new OnTaskCompleted() {
      @Override
      public void onTaskCompleted(@NonNull TaskOutcome taskOutcome) {
          if (taskOutcome == TaskOutcome.COMPLETED) {
              // Task completed
          } else {
              // Task cancelled
          }
      }
  });
```

This data source can then be attached to the `OfflineSearchEngine`:

```java
offlineSearchEngine.attach(myPlaces, new OnTaskCompleted() {
    @Override
    public void onTaskCompleted(@NonNull 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 `TaskOutcome`event informs when the operation has finished.

Below is one example how you can find such places:

```java
int radiusInMeters = 100;
TextQuery.Area queryArea = new TextQuery.Area(new GeoCircle(new GeoCoordinates(52.518032, 13.420632), radiusInMeters));
TextQuery textQuery = new TextQuery("Pizza Pino", queryArea);
offlineSearchEngine.searchByText(textQuery, new SearchOptions(), new SearchCallback() {
    @Override
    public void onSearchCompleted(@Nullable SearchError searchError, @Nullable List<Place> list) {
        // ...
    }
});
```

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.

```java
void testDistributedResultsWithCorridor() {
    // Create a corridor area for the search - this is required for distributedResults to work
    List<GeoCoordinates> corridorPoints = Arrays.asList(
            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
    );

    CategoryQuery.Area queryArea = new CategoryQuery.Area(new GeoCorridor(corridorPoints), new GeoCoordinates(52.518032, 13.420632));

    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, queryArea);

    SearchOptions searchOptions = new SearchOptions();
    searchOptions.languageCode = LanguageCode.EN_US;
    searchOptions.maxItems = 50;

    searchOptions.distributedResults = true;

    offlineSearchEngine.searchByCategory(categoryQuery, searchOptions, new SearchCallback() {
        @Override
        public void onSearchCompleted(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.size() + ". See log for details.";

            for (int i = 0; i < list.size(); i++) {
                Place searchResult = list.get(i);
                String name = searchResult.getTitle();
                String address = searchResult.getAddress().addressText;
                GeoCoordinates coords = searchResult.getGeoCoordinates();

                Log.d("DistributedSearch", String.format(
                        "Result %d/%d: %s at %s (%.6f, %.6f)",
                        i+1, list.size(), name, address, coords.latitude, coords.longitude
                ));
            }
        }
    });
  }
```

## Enable enhanced offline search globally

The HERE SDK provides an enhanced offline search algorithm via the `OFFLINE_SEARCH_GLOBAL` option in `LayerConfiguration`. This enables globally-aware search over all downloaded map regions without requiring additional indexing.

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

To enable it, configure `LayerConfiguration` with `OFFLINE_SEARCH_GLOBAL` during HERE SDK initialization. Note that `OFFLINE_SEARCH` should be omitted to avoid downloading layers that are no longer needed:

```java Java
ArrayList<LayerConfiguration.Feature> features = new ArrayList<>();
// Note: enabledFeatures(..) and implicitlyPrefetchedFeatures(..) can be set to different feature configurations (see below).
features.add(LayerConfiguration.Feature.DETAIL_RENDERING);
features.add(LayerConfiguration.Feature.RENDERING);
features.add(LayerConfiguration.Feature.NAVIGATION);
features.add(LayerConfiguration.Feature.OFFLINE_ROUTING);
// Add OFFLINE_SEARCH_GLOBAL to enable the enhanced algorithm for downloaded map regions.
// Note: OFFLINE_SEARCH is intentionally omitted. To prevent growth of downloaded and cached map data,
// it is recommended to enable only one of OFFLINE_SEARCH_GLOBAL or OFFLINE_SEARCH at a time.
features.add(LayerConfiguration.Feature.OFFLINE_SEARCH_GLOBAL);

LayerConfiguration layerConfiguration = new LayerConfiguration();
// Enabling this feature increases storage requirements: downloaded map region size by ~11–16% when enabled via enabledFeatures.
layerConfiguration.enabledFeatures = features;
// While enabledFeatures(..) defines the feature configuration for explicitly downloaded map regions;
// implicitlyPrefetchedFeatures(..) defines the feature configuration for map data that is
// prefetched implicitly when displaying a map in MapView, or explicitly via RoutePrefetcher and PolygonPrefetcher.
// Enabling implicitlyPrefetchedFeatures is optional.
// Note: layerConfiguration.implicitlyPrefetchedFeatures can be a subset of
// layerConfiguration.enabledFeatures, but not vice versa.
// Passing the same feature configuration for both is not a requirement.
// Map cache size by ~40–140% when enabled via implicitlyPrefetchedFeatures (see API Reference for details).
layerConfiguration.implicitlyPrefetchedFeatures = features;

SDKOptions sdkOptions = new SDKOptions(authenticationMode);
sdkOptions.layerConfiguration = layerConfiguration;

try {
    SDKNativeEngine.makeSharedInstance(context, sdkOptions);
} catch (InstantiationErrorException e) {
    throw new RuntimeException("Initialization of HERE SDK failed: " + e.error.name());
}
// After changing the layer configuration, update any previously downloaded offline maps
// to avoid SearchError.LAYERS_NOT_DOWNLOADED or SearchError.MAP_NOT_READY errors.
// Use mapUpdater.performMapUpdate() or mapUpdater.updateCatalog() to update the maps.
```

```kotlin Kotlin
val features = ArrayList<LayerConfiguration.Feature>()
// Note: enabledFeatures(..) and implicitlyPrefetchedFeatures(..) can be set to different feature configurations (see below).
features.add(LayerConfiguration.Feature.DETAIL_RENDERING)
features.add(LayerConfiguration.Feature.RENDERING)
features.add(LayerConfiguration.Feature.NAVIGATION)
features.add(LayerConfiguration.Feature.OFFLINE_ROUTING)
// Add OFFLINE_SEARCH_GLOBAL to enable the enhanced algorithm for downloaded map regions.
// Note: OFFLINE_SEARCH is intentionally omitted. To prevent growth of downloaded and cached map data,
// it is recommended to enable only one of OFFLINE_SEARCH_GLOBAL or OFFLINE_SEARCH at a time.
features.add(LayerConfiguration.Feature.OFFLINE_SEARCH_GLOBAL)

val layerConfiguration = LayerConfiguration()
// Enabling this feature increases storage requirements: downloaded map region size by ~11–16% when enabled via enabledFeatures.
layerConfiguration.enabledFeatures = features
// While enabledFeatures(..) defines the feature configuration for explicitly downloaded map regions;
// implicitlyPrefetchedFeatures(..) defines the feature configuration for map data that is
// prefetched implicitly when displaying a map in MapView, or explicitly via RoutePrefetcher and PolygonPrefetcher.
// Enabling implicitlyPrefetchedFeatures is optional.
// Note: layerConfiguration.implicitlyPrefetchedFeatures can be a subset of
// layerConfiguration.enabledFeatures, but not vice versa.
// Passing the same feature configuration for both is not a requirement.
// Map cache size by ~40–140% when enabled via implicitlyPrefetchedFeatures (see API Reference for details).
layerConfiguration.implicitlyPrefetchedFeatures = features

val sdkOptions = SDKOptions(authenticationMode)
sdkOptions.layerConfiguration = layerConfiguration

try {
    SDKNativeEngine.makeSharedInstance(context, sdkOptions)
} catch (e: InstantiationErrorException) {
    throw RuntimeException("Initialization of HERE SDK failed: " + e.error.name)
}
// After changing the layer configuration, update any previously downloaded offline maps
// to avoid SearchError.LAYERS_NOT_DOWNLOADED or SearchError.MAP_NOT_READY errors.
// Use mapUpdater.performMapUpdate() or mapUpdater.updateCatalog() to update the maps.
```

> #### Note
>
> After switching from `OFFLINE_SEARCH` to `OFFLINE_SEARCH_GLOBAL`, update any previously downloaded offline maps using `mapUpdater.performMapUpdate()` or `mapUpdater.updateCatalog()`. Failing to do so may result in `SearchError.LAYERS_NOT_DOWNLOADED` or `SearchError.MAP_NOT_READY` errors.