Search Places

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.

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.

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 SearchOptions 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. In the example above, 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.

A one-box search is ideal to discover places nearby. As input you can provide a free-form text in various and mixed languages (Latin, Cyrillic, Arabic, Greek, ...).

You can specify the area together with the term you want to search for. For queryString you can set, for example, "pizza":

let queryArea = TextQuery.Area(inBox: getMapViewGeoBox())
let textQuery = TextQuery(queryString, area: queryArea)

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! {
        //...
    }
}

Note

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.

Note

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))
    // Note that geoCoordinates are always set, but can be nil for suggestions only.
    addPoiMapMarker(geoCoordinates: searchResult.geoCoordinates!, 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.

Note

You can find the full code for this and the following sections as part of the Search example app on GitHub.

Zoom to Places

The above code uses a GeoBox to search directly in the shown map viewport, so it is not necessary to zoom to the found results. Instead of a GeoBox you can search also around a location ("search around me"), inside a GeoCircle, along a GeoCorridor or inside countries by passing a list of CountryCode values. See TextQuery.Area for all supported types.

If the search area is not equal to the shown map viewport, you can use the following code to zoom to the results by creating a GeoBox from a list of GeoCoordinates. Get the GeoBox from GeoBox.containing(geoCoordinates: geoCoordinatesList).

// Set null values to keep the default map orientation.
camera.lookAt(area: geoBox,
              orientation: GeoOrientationUpdate(bearing: nil, tilt: nil))

This instantly moves the camera. If desired, you can also apply various animation styles to fly the camera to a desired area. Look at the MapCamera section and check the CameraKeyframeTracks example app on GitHub.

If you want to apply an additional padding, use an overloaded lookAt() method that accepts a viewRectangle as additional parameter. Note that the rectangle is specified in pixels referring to the map view inside which the GeoBox is displayed.

let origin = Point2D(5, 5)
let sizeInPixels = Size2D(width: mapView.viewportSize.width - 10, height: mapView.viewportSize.height - 10)
let paddedViewRectangle = Rectangle2D(origin: origin, size: sizeInPixels)

The above code creates a rectangle that can be used to add a 5 pixel padding around any GeoBox that is shown in the map viewport.

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

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 implementd 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.suggest(textQuery: TextQuery("p", area: queryArea),
                         options: autosuggestOptions,
                         completion: onSearchCompleted)

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

_ = searchEngine.suggest(textQuery: 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 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. 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.

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.

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:

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.

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.

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 = TextQuery.Area(inCorridor: routeCorridor, near: mapView.camera.state.targetCoordinates)
    let textQuery = TextQuery("charging station", area: queryArea)

    let searchOptions = SearchOptions(languageCode: LanguageCode.enUs,
                                      maxItems: 30)
    searchEngine.search(textQuery: textQuery,
                        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 {
        if searchError == .polylineTooLong {
            // Increasing halfWidthInMeters will result in less precise results with the benefit of a less
            // complex route shape.
            print("Route too long or halfWidthInMeters too small.")
        } else {
            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! {
        // ...
    }
}

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.

Especially for longer routes, internally the search algorithm will try to optimize the search corridor. However, it may happen that a polyline is too long. As shown in the code snippet above, you can catch this case and then eventually decide to retrigger a search for a less complex route: This can be controlled by the halfWidthInMeters parameter - a larger value will decrease the complexity of the corridor and therefore allow less precise results, but at least you will find more results this way.

Note that the complexity of a route is determined by several factors under the hood, so no definite length for a route can be given in general.

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.

results matching ""

    No results matching ""