Search

Sourced from HERE's global data set of hundreds of millions of POIs and point addresses worldwide, the HERE SDK for iOS 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 OfflineEngine to search on already cached map 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.

In addition, the HERE SDK for iOS allows searching for nearby and remote traffic incidents via a dedicated TrafficEngine that shares similar interfaces and concepts. A more detailed description of the traffic engine is given here.

Note: Each search request is performed asynchronously. An online connection is required.

The massive database of places provided by HERE's 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:

do {
    try searchEngine = SearchEngine()
} catch let engineInstantiationError {
    fatalError("Failed to initialize SearchEngine. Cause: \(engineInstantiationError)")
}

Creating a new SearchEngine instance can throw an error that we have to handle as shown above. Such an error can happen, when, for example, the HERE SDK initialization failed beforehand.

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:

let searchOptions = SearchOptions(languageCode: LanguageCode.enUs,
                                  maxItems: 30)

From the code snippet above, we created a new Options object that holds 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, so 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 geographical 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 together with the term you want to search for. For queryString you can set, for example, "pizza":

let textQuery = TextQuery(queryString, in: getMapViewGeoBox())

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.search(textQuery: textQuery,
                        options: searchOptions,
                        completion: onSearchCompleted)

...

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

    // If error is nil, it is guaranteed that the items will not be nil.
    showDialog(title: "Search in viewport for: 'Pizza'.",
               message: "Found  \(items!.count) results.")

    // Add a new marker for each search result on map.
    for searchResult in items! {
        //...
    }
}

Alternatively, a closure expression can be used to inline the completion handler:

_ = searchEngine.search(textQuery: textQuery,
                        options: searchOptions) { (searchError, searchResultItems) in
   // Handle results here.
}

Of course, the same is possible for all other completion handling available in the HERE SDK. By convention, for this guide, we prefer to use function call expressions to preserve the full type information.

Note that all offered search() methods return a TaskHandle that can be optionally used to check the status of an ongoing call - or to cancel a call. If not needed, you can leave this out, as shown above.

Before we can look into the results, we should check for a possible SearchError. For example, if the device is offline, the list of search items will be nil 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 that we can safely unwrap the search items list, as we opt out beforehand if an error occurs.

The search response contains either an error or a result: error and items can never be nil at the same time - or non-nil 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:

// If error is nil, it is guaranteed that the items will not be nil.
showDialog(title: "Search in viewport for: 'Pizza'.",
           message: "Found  \(items!.count) results.")

// Add a new marker for each search result on map.
for searchResult in items! {
    let metadata = Metadata()
    metadata.setCustomValue(key: "key_search_result", value: SearchResultMetadata(searchResult))
    addPoiMapMarker(geoCoordinates: searchResult.coordinates, metadata: metadata)
}

...

// Class to store search results as Metadata.
private class SearchResultMetadata : CustomMetadataValue {

    var searchResult: Place

    init(_ searchResult: Place) {
        self.searchResult = searchResult
    }

    func getTag() -> String {
        return "SearchResult Metadata"
    }
}

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 map marker 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, you can get the Metadata information that we have set in the previous step:

if let searchResultMetadata =
    topmostMapMarker.metadata?.getCustomValue(key: "key_search_result") as? SearchResultMetadata {

    let title = searchResultMetadata.searchResult.title
    let vicinity = searchResultMetadata.searchResult.address.addressText
    showDialog(title: "Picked Search Result",
               message: "Title: \(title), Vicinity: \(vicinity)")
    return
}

Not all map markers may contain Metadata. Unless you have set the Metadata beforehand, getMetadata() will return nil. In this example, we simply check if the data stored for "key_search_result" is not nil, 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 optional fields.

Screenshot: Showing a picked search result with title and vicinity.

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:

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

    _ = searchEngine.search(categoryQuery: 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 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.

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

// Simulate a user typing a search term.
_ = searchEngine.suggest(textQuery: TextQuery("p", near: centerGeoCoordinates),
                         options: autosuggestOptions,
                         completion: onSearchCompleted)

_ = searchEngine.suggest(textQuery: TextQuery("pi", near: centerGeoCoordinates),
                         options: autosuggestOptions,
                         completion: onSearchCompleted)

_ = searchEngine.suggest(textQuery: TextQuery("piz", near: centerGeoCoordinates),
                         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 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.

// 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. In such a case, 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.

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

private func getAddressForCoordinates(geoCoordinates: GeoCoordinates) {
    // By default results are localized in EN_US.
    let reverseGeocodingOptions = SearchOptions(languageCode: LanguageCode.enGb,
                                                maxItems: 1)
    _ = searchEngine.search(coordinates: 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 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.

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

let query = AddressQuery(queryString, near: geoCoordinates)
let geocodingOptions = SearchOptions(languageCode: LanguageCode.deDe,
                                     maxItems: 25)
_ = searchEngine.search(addressQuery: 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.

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! {
    let geoCoordinates = geocodingResult.coordinates
    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.

results matching ""

    No results matching ""