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

Search & Geocoding features

The HERE SDK offers a comprehensive suite of search and geocoding features that allow you to interact with geographical data seamlessly. Whether you're looking to convert coordinates into human-readable addresses or find locations based on address input, the HERE SDK has you covered.

Reverse geocode an address from geographic coordinates

With the SearchEngine, searching for places at specific locations or areas on the map is straightforward. But what if we only have a location's coordinates? A common scenario involves a user interacting with the map, such as performing a long press gesture, which provides the latitude and longitude of the selected location. While the user can see this location on the map, we lack additional details like the corresponding address information.

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

private func getAddressForCoordinates(geoCoordinates: GeoCoordinates) {
    // By default results are localized in EN_US.
    let reverseGeocodingOptions = SearchOptions(languageCode: LanguageCode.enGb,
                                                maxItems: 1)
    _ = searchEngine.searchByCoordinates(geoCoordinates,
                            options: reverseGeocodingOptions,
                            completion: onReverseGeocodingCompleted)
}

// Completion handler to receive reverse geocoding results.
func onReverseGeocodingCompleted(error: SearchError?, items: [Place]?) {
    if let searchError = error {
        showDialog(title: "ReverseGeocodingError", message: "Error: \(searchError)")
        return
    }

    // If error is nil, it is guaranteed that the place list will not be empty.
    let addressText = items!.first!.address.addressText
    showDialog(title: "Reverse geocoded address:", message: 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 searchByAddress()-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 items list can never be nil at the same time - or non-nil 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 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:

// The geoCoordinates act as a reference location to prioritize the search results.
// This helps the `SearchEngine` return addresses that are more relevant and closer to the user’s
// current location instead of global or less relevant matches.
let query = AddressQuery(queryString, near: geoCoordinates)
let geocodingOptions = SearchOptions(languageCode: LanguageCode.deDe,
                                     maxItems: 25)
_ = searchEngine.searchByAddress(query,
                        options: geocodingOptions,
                        completion: onGeocodingCompleted)

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.

As a next step, we must implement the completion handler:

// Completion handler to receive geocoding results.
func onGeocodingCompleted(error: SearchError?, items: [Place]?) {
    if let searchError = error {
        showDialog(title: "Geocoding", message: "Error: \(searchError)")
        return
    }

    // If error is nil, it is guaranteed that the items will not be nil.
    for geocodingResult in items! {
        //...
    }
}

After validating that the completion handler received no error, we check the list for Place elements.

Note

If searchError is nil, it is guaranteed that the resulting items will not be nil, and vice versa. Therefore it's safe to unwrap the optional list.

The results are wrapped in a Places 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 geocodingResult in items! {
    // Note that geoCoordinates are always set, but can be nil for suggestions only.
    let geoCoordinates = geocodingResult.geoCoordinates!
    let address = geocodingResult.address
    let locationDetails = address.addressText
        + ". Coordinates: \(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.

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

Note

The HERE SDK does not provide any UI or a fully integrated auto completion solution. Such a solution can be implemented by an application, if desired. With the Suggestion feature you get possible Place results based on a TextQuery: from these places you can use the title text ("Pizza XL") or other relevant place information (such as addresses) to provide feedback to a user - for example, to propose a clickable completion result. However, such a solution depends on the individual requirements of an application and needs to be implemented on app side using platform APIs.

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.

let centerGeoCoordinates = getMapViewCenter()
let autosuggestOptions = SearchOptions(languageCode: LanguageCode.enUs,
                                       maxItems: 5)

let queryArea = TextQuery.Area(areaCenter: centerGeoCoordinates)

// Simulate a user typing a search term.
_ = searchEngine.suggestByText(TextQuery("p", area: queryArea),
                         options: autosuggestOptions,
                         completion: onSearchCompleted)

_ = searchEngine.suggestByText(TextQuery("pi", area: queryArea),
                         options: autosuggestOptions,
                         completion: onSearchCompleted)

_ = searchEngine.suggestByText(TextQuery("piz", area: queryArea),
                         options: autosuggestOptions,
                         completion: onSearchCompleted)

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.

Similar to the other search() methods from SearchEngine, the suggestByText()-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.

// Completion handler to receive auto suggestion results.
func onSearchCompleted(error: SearchError?, items: [Suggestion]?) {
    if let searchError = error {
        print("Autosuggest Error: \(searchError)")
        return
    }

    // If error is nil, it is guaranteed that the items will not be nil.
    print("Autosuggest: Found \(items!.count) result(s).")

    for autosuggestResult in items! {
        var addressText = "Not a place."
        if let place = autosuggestResult.place {
            addressText = place.address.addressText
        }
        print("Autosuggest result: \(autosuggestResult.title), addressText: \(addressText)")
    }
}

Here we define a completion handler 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 nil.

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 completion events arrive. So, in rare cases, you may receive the "piz" results before the "pi" results.

Note (only applicable for Navigate)

You can use suggestions also with the OfflineSearchEngine. In this case, just swap the SearchEngine with an instance of the OfflineSearchEngine. Note that this requires downloaded or cached map data. You can find an example for this in the "SearchHybrid" example app you can find on GitHub.

Search for places categories

Instead of doing a keyword search using TextQuery like "Pizza", 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 first, second and third level categories. While first level represents the main category, third level represents the sub category of the second 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:

private func searchForCategories() {
    let categoryList = [PlaceCategory(id: PlaceCategory.eatAndDrink),
                        PlaceCategory(id: PlaceCategory.shoppingElectronics)]
    let queryArea = CategoryQuery.Area(areaCenter: GeoCoordinates(latitude: 52.520798,
                                                                  longitude: 13.409408))
    let categoryQuery = CategoryQuery(categoryList, area: queryArea)
    let searchOptions = SearchOptions(languageCode: LanguageCode.enUs,
                                      maxItems: 30)

    _ = searchEngine.searchByCategory(categoryQuery,
                            options: searchOptions,
                            completion: onSearchCompleted)
}

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

    // If error is nil, it is guaranteed that the items will not be nil.
    showDialog(title: "Search Result", message: "\(items!.count) result(s) found. See log for details.")

    for place in items! {
        let addressText = place.address.addressText
        print(addressText)
    }
}

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 searchByCategory() 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 place chains

Place chain allows to search for all places that belong to a certain brand. For example, if the ID represents "K-Supermarket", then you can limit your search to find only "K-Supermarket" stores. Make sure that the PlaceCategory will match the place chain's category.

The PlaceChain identifier is used to associate a place chain. The list of supported place chains can be found at this link.

let placeChain = PlaceChain(id: "1566")
var includeChains: [PlaceChain] = []
includeChains.append(placeChain)
categoryQuery.includeChains = includeChains

The above chain ID "1566" identifies the "McDonald's" place chain, which falls under the eatAndDrink category. If you set multiple place categories, then at least one category needs to match or no results will be found.

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:

let textQuery = TextQuery("restaurants", in: route.boundingBox)

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 func searchAlongARoute(route: Route) {
    // We specify here that we only want to include results
    // within a max distance of xx meters from any point of the route.
    let routeCorridor = GeoCorridor(polyline: route.geometry.vertices,
                                    halfWidthInMeters: Int32(200))
    let queryArea = CategoryQuery.Area(inCorridor: routeCorridor, near: mapView.camera.state.targetCoordinates)
    let placeCategory = PlaceCategory(id: PlaceCategory.businessAndServicesEvChargingStation)
    let categoryQuery = CategoryQuery(placeCategory, area: queryArea)

    let searchOptions = SearchOptions(languageCode: LanguageCode.enUs,
                                      maxItems: 30)
    searchEngine.searchByCategory(categoryQuery,
                        options: searchOptions,
                        completion: onSearchCompleted)
}

// Completion handler to receive results for found charging stations along the route.
func onSearchCompleted(error: SearchError?, items: [Place]?) {
    if let searchError = error {
        print("No charging stations found along the route. Error: \(searchError)")
        return
    }

    // If error is nil, it is guaranteed that the items will not be nil.
    print("Search along route found \(items!.count) charging stations:")

    for place in items! {
        // ...
    }
}

Fetching availability status for EV charging stations

Availability information, including the total number of connectors, occupied and available connectors, and additional details. The enableEVChargingStationDetails() option enables online retrieval of EV charging station availability, including the status of individual charging stations.

Attempting to use this feature without enabled credentials will result in a SearchError. For more information, please refer to the API reference.

This feature requires a custom option call as shown below:

// Enable fetching online availability details for EV charging stations.
// It allows retrieving additional details, such as whether a charging station is currently occupied.
// Check the API Reference for more details.
private func enableEVChargingStationDetails() {
    // Fetching additional charging stations details requires a custom option call.
    if let error = searchEngine.setCustomOption(name: "browse.show", value: "ev") {
    showDialog(
        title: "Charging Station",
        message: "Failed to enableEVChargingStationDetails.")
    } else {
        print("EV charging station availability enabled successfully.")
    }
}

// Perform a search for charging stations along the found route.
private func searchAlongARoute(route: Route) {
    // We specify here that we only want to include results
    // within a max distance of xx meters from any point of the route.
    let routeCorridor = GeoCorridor(polyline: route.geometry.vertices,
                                    halfWidthInMeters: Int32(200))
    let queryArea = CategoryQuery.Area(inCorridor: routeCorridor, near: mapView.camera.state.targetCoordinates)
    let placeCategory = PlaceCategory(id: PlaceCategory.businessAndServicesEvChargingStation)
    let categoryQuery = CategoryQuery(placeCategory, area: queryArea)

    let searchOptions = SearchOptions(languageCode: LanguageCode.enUs,
                                        maxItems: 30)
    enableEVChargingStationDetails()
    
    searchEngine.searchByCategory(categoryQuery,
                        options: searchOptions,
                        completion: onSearchCompleted)
}

// Completion handler to receive results for found charging stations along the route.
func onSearchCompleted(error: SearchError?, items: [Place]?) {
    if let searchError = error {
        print("No charging stations found along the route. Error: \(searchError)")
        return
    }

    // If error is nil, it is guaranteed that the items will not be nil.
    print("Search along route found \(items!.count) charging stations:")

    for place in items ?? [] {
        let details = place.details
        let metadata = getMetadataForEVChargingPools(details)
        var foundExistingChargingStation = false

        for mapMarker in mapMarkers {
            if let markerMetadata = mapMarker.metadata,
                let id = markerMetadata.getString(key: requiredChargingMetadataKey),
                id.lowercased() == place.id.lowercased() {
                print("Skipping: This charging station was already required to reach the destination (see red charging icon).")
                mapMarker.metadata = metadata
                foundExistingChargingStation = true
                break
            }
        }
        if !foundExistingChargingStation {
            self.addMapMarker(geoCoordinates: place.geoCoordinates!, imageName: "charging.png", metadata: metadata)
        }
    }
}

func getMetadataForEVChargingPools(_ placeDetails: Details) -> Metadata {
    let metadata = Metadata()
    
    if let chargingPool = placeDetails.evChargingPool {
        for station in chargingPool.chargingStations {

            if let supplierName = station.supplierName {
                metadata.setString(key: supplierNameMetadataKey, value: supplierName)
            }
            if let connectorCount = station.connectorCount {
                metadata.setString(key: connectorCountMetadataKey, value: "\(connectorCount)")
            }
            if let availableConnectorCount = station.availableConnectorCount {
                metadata.setString(key: availableConnectorsMetadataKey, value: "\(availableConnectorCount)")
            }
            if let occupiedConnectorCount = station.occupiedConnectorCount {
                metadata.setString(key: occupiedConnectorsMetadataKey, value: "\(occupiedConnectorCount)")
            }
            if let outOfServiceConnectorCount = station.outOfServiceConnectorCount {
                metadata.setString(key: outOfServiceConnectorsMetadataKey, value: "\(outOfServiceConnectorCount)")
            }
            if let reservedConnectorCount = station.reservedConnectorCount {
                metadata.setString(key: reservedConnectorsMetadataKey, value: "\(reservedConnectorCount)")
            }
            if let lastUpdated = station.lastUpdated {
                metadata.setString(key: lastUpdatedMetadataKey, value: "\(lastUpdated)")
            }
        }
    }
    return metadata
}

See the screenshot below for an example of how this appears when you click on a charging station. A pop-up will display the charging station name, connector count, available connectors, last updated time, and more.

Screenshot: Showing charging station availability details.

As you can see, the GeoCorridor requires the route's GeoPolyline and halfWidthInMeters. This parameter 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.

For very long routes, internally the search algorithm will try to optimize the search corridor. This can be controlled also on app-side with the halfWidthInMeters parameter - a larger value will decrease the complexity of the corridor and therefore result in less precise results as a trade-off.

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.