Skip to content

Search

This section explains how to implement online/offline search in iOS application.

Overview

Search is designed to resolve a free-formatted text query to an exact destination on a map (including title, location, additional info). Also it can search for places of selected categories in requested area. Related classes: SYSearchBase & child classes (e.g. SYOfflineMapSearch), SYSearchSession, SYSearchRequest, SYSearchResult.

Reverse search takes location (SYGeoCoordinate) and resolves it to an address (city, street, house number). Related classes: SYReverseSearch, SYReverseSearchResult.

Info

SYOnlineSession.onlineMapsEnabled parameter affects Reverse search, and doesn't affect Search.

Search engines

Various search engines can be used separately or combined with each other:

  • SYCoordinateSearch - recognizes SYGeoCoordinate by search input like "48.1457, 17.1269"
  • SYOfflineMapSearch - searches for cities, streets, places in map data already downloaded by SYMapInstaller
  • SYOnlineMapSearch - searches for cities, streets, places using online map service
  • SYFlatDataSearch - searches in user-provided flat data items (title, subtitle, location)
  • SYCustomPlacesSearch - searches for places already installed by SYCustomPlacesManager

Warning

SYOfflineMapSearch takes a lot of system resources, we recommend to share one instance across the app, creating many independent sessions if needed (or many instances of SYSearchSessionProvider).

SYCustomPlacesSearch might take some time to initialize because of indexing process, we also recommend to share one instance across the app.

Composite search engine

To combine different search types, use a composite search. It distributes a request to all search engines it contains.

  • SYCompositeSequentialSearch - calls search engines one by one, until something is found (if one search provides results, next search engines are not called)
  • SYCompositeParallelSearch - performs search with all searches at the same time, results are joined into one list

Info

  1. SYSearchBase is a parent class for all search engines
  2. A search engine can be used separately while being a part of one or more composite searches
  3. A composite search can be a part of other composite searches

The following example creates a complex search for a navigation app (it can be used as a replacement of SYSearch, which is deprecated).

func makeComplexSearch() -> SYSearchBase {
    let map = SYOfflineMapSearch()
    let coordinate = SYCoordinateSearch()
    let favorites = SYFlatDataSearch(priority: 1.0) // should be filled with data later
    let history = SYFlatDataSearch(priority: 0.75) // should be filled with data later

    return SYCompositeSequentialSearch(
        searches: [
            coordinate, // this goes first to quickly react when user enters exact coordinate
            SYCompositeParallelSearch(searches: [map, favorites, history])
        ]
    )
}

Search request types

Every search engine can create SYSearchSession. Search session allows to perform search requests on the corresponding search engine. There are 4 types of requests:

  • Autocomplete - quick and lightweight, providing immediate suggestions while user is typing a text query, results can be shown in a list but not on the map
  • Geocode Location - takes Location ID (from Autocomplete result in the same session) and provides full information about this exact search result
  • Geocode - takes input text query, delivers matched results with full information, it makes sense when a user finished typing and pressed Search button
  • Places - finds places by category in the selected area, results contain full information

Session lifecycle

Search engine may have more than one active session at once, but lifecycle of each session is limited.

Search session instance can perform any amount of Autocomplete requests, and only one of these: Geocode Location, Geocode, or Places. After that the session is finished, next search will be possible only in the next session.

Lifecycle examples:

- New session => Autocomplete => Geocode Location
- New session => Autocomplete => Autocomplete => Autocomplete => Geocode Location
- New session => Geocode
- New session => Autocomplete => Geocode
- New session => Autocomplete => Autocomplete => Geocode
- New session => Places
- New session => Autocomplete => Places
- New session => Autocomplete => Autocomplete => Places

To simplify the work with lifecycle, use SYSearchSessionProvider method session(). If the last returned session is still valid, it will be returned again, otherwise a new session will be created.

Info

Inside of one search session, every next request cancels the previous request, if it's still in progress. For independent request processing, create several sessions.

Autocomplete and Geocode

Assuming SYContext is already initialized, this example shows Autocomplete, Geocode Location and Geocode requests. Results are printed to debug console in methods printAutocompleteResult, printDetailedResult.

class DemoSearch {
    let search: SYSearchBase = SYOnlineMapSearch()

    func demoAutocomplete() {
        let request = SYSearchRequest(query: "tovarenska", location: SYGeoCoordinate(latitude: 48.1457, longitude: 17.1269))
        request.maxResultsCount = 3

        let session = search.startNewSession()
        session.autocomplete(request) { (results: [SYSearchAutocompleteResult]?, status: SYSearchStatus) in

            guard status == .success, let results = results else {
                print("MYLOG: ERROR: Autocomplete status=\(status.rawValue)")
                return
            }

            results.forEach { (result) in
                self.printAutocompleteResult(result)
            }
        }
    }

    func demoAutocompleteAndGeocodeLocation() {
        let request = SYSearchRequest(query: "tovarenska", location: SYGeoCoordinate(latitude: 48.1457, longitude: 17.1269))
        request.maxResultsCount = 3

        let session = search.startNewSession()
        session.autocomplete(request) { (suggestions: [SYSearchAutocompleteResult]?, status: SYSearchStatus) in

            guard status == .success, let suggestions = suggestions else {
                print("MYLOG: ERROR: Autocomplete status=\(status.rawValue)")
                return
            }
            guard let firstLocationId = suggestions.first?.locationId else {
                print("MYLOG: Autocomplete no results")
                return
            }

            let request = SYGeocodeLocationRequest(locationId: firstLocationId)
            session.finish(byRequestingGeocodeLocation: request) { (result: SYSearchGeocodingResult?, status: SYSearchStatus) in

                guard status == .success, let result = result else {
                    print("MYLOG: ERROR: GeocodeLocation status=\(status.rawValue)")
                    return
                }

                self.printDetailedResult(result)
            }
        }
    }

    func demoGeocode() {
        let request = SYSearchRequest(query: "airport", location: SYGeoCoordinate(latitude: 48.1457, longitude: 17.1269))

        let session = search.startNewSession()
        session.finish(byRequestingGeocode: request) {
            (results: [SYSearchGeocodingResult]?, status: SYSearchStatus) in

            guard status == .success, let results = results else {
                print("MYLOG: ERROR: Geocode status=\(status.rawValue)")
                return
            }

            results.forEach { (result) in
                self.printDetailedResult(result)
            }
        }
    }

    func printAutocompleteResult(_ result: SYSearchAutocompleteResult) {
        let title = result.title?.value ?? ""
        print("MYLOG: Autocomplete: '\(title)', type=\(result.type.rawValue), distance=\(result.distance)")
    }

    func printDetailedResult(_ result: SYSearchGeocodingResult) {
        let title = result.title?.value ?? ""
        let lat = result.location?.latitude ?? 0
        let lon = result.location?.longitude ?? 0
        print("MYLOG: GeocodingResult: '\(title)', type=\(result.type.rawValue)); \(lat),\(lon)")

        if let mapResult = result as? SYSearchMapResult {
            // mapResult provides more specific info
        } else if let flatDataResult = result as? SYSearchFlatDataResult {
            // flatDataResult provides more specific info
        } // else if let ... SYSearch...Result
    }
}

Places

You can also search places around a specific coordinate in a radius or in a Bounding Box. Here's an example on how to do it.

Info

To request more places with the same request, use SYSearchMorePlacesSession provided in completion. The number of places per page is configured with request.maxResultsCount.

extension DemoSearch  {

    func placesRequestLocation() -> SYSearchPlacesRequest {
        SYSearchPlacesRequest(
            location: SYGeoCoordinate(latitude: 48.1457, longitude: 17.1269),
            radius: 10000,
            tags: [SYPlaceCategoryPetrolStation, SYPlaceCategoryPharmacy]
        )
    }

    func placesRequestBoundingBox() -> SYSearchPlacesRequest {
        SYSearchPlacesRequest(
            boundingBox: SYGeoBoundingBox(
                topLeft: SYGeoCoordinate(latitude: 48.1495, longitude: 17.1038),
                bottomRight: SYGeoCoordinate(latitude: 48.1395, longitude: 17.1181)
            ),
            tags: [SYPlaceCategoryRestaurant]
        )
    }

    func demoPlaces() {
        let request = placesRequestLocation() //or placesRequestBoundingBox()

        let session = search.startNewSession()
        session.finish(byRequestingPlaces: request) {
            (places: [SYPlace]?, morePlacesSession: SYSearchMorePlacesSession?, status: SYSearchStatus) in

            guard status == .success, let places = places else {
                print("MYLOG: ERROR: Places status=\(status.rawValue)")
                return
            }

            places.forEach { (place) in
                self.printPlace(place)
            }

            // morePlacesSession can be saved and then used to get more places for the same request
        }
    }

    func printPlace(_ place: SYPlace) {
        print("MYLOG: ---")
        let lat = place.link.coordinate.latitude
        let lon = place.link.coordinate.longitude
        print("MYLOG: Place name: \(place.link.name); cat: \(place.link.category); \(lat),\(lon)")
        place.details.forEach { (key, value) in
            print("MYLOG: Place detail: \(key): \(value)")
        }
    }
}

Reverse search (also called Reverse geocoding) converts a location (SYGeoCoordinate) to an address (country, city, street, house number). Current state of SYOnlineSession.onlineMapsEnabled affects if the online or offline map data is used. For offline, the corresponding country must be already downloaded by SYMapInstaller.

class DemoReverseGeocoding {
    let search = SYReverseSearch()

    func reverseSearch() {
        let coordinate = SYGeoCoordinate(latitude: 48.146410, longitude: 17.138270)

        search.reverseSearch(with: coordinate, withFilter: nil) { (results: [SYReverseSearchResult]?, error) in
            guard let results = results else {
                print("SYReverseSearch error: \(String(describing: error))")
                return
            }
            results.forEach { $0.printDescription() }
        }
    }
}

extension SYReverseSearchResult {
    func printDescription() {
        let description = resultDescription
        print("""
            \nSYReverseSearchResult:
            country: \(description.countryIso)
            city: \(String(describing: description.city))
            street: \(String(describing: description.street))
            house number: \(String(describing: description.houseNumber))
            distance: \(distance)
            """)
    }
}

Get the timezone of a location

One of the two prerequisites need to be met in order to get the time zone of a location, otherwise the operation will finish with error:

  • The coordinate must be inside of already downloaded maps
  • Online maps need to be active

Asynchronously returns time zone (in seconds from GMT) at a specific location and in a specific time (result may differ for summer/winter time). It's recommended to use the result for creation of a NSTimeZone object.

Here we use the current time to get the seconds from GMT at a location and directly convert it to a TimeZone object.

SYReverseSearch().getTimeZone(atLocation: SYGeoCoordinate(latitude: 12.34, longitude: 56.78), atTime: Date()) { result, error in
    let timezone = TimeZone(secondsFromGMT: result as! Int)
}

Custom Places Search is a search engine that searches for places that were installed by SYCustomPlacesManager. It can be used separately or as a part of a composite search.

If you are using the Online mode where the places are not downloaded to the device, you can still search for the custom places that are stored on the server. These places are automatically included in the results when calling the SYOnlineMapSearch's searchPlaces() or autocomplete() or geocode() function. You can read about them in the previous chapters.

To search for Custom Places that are stored locally, you can create the SYCustomPlacesSearch class which you can use directly for searching or add it to a composite/parallel search.

Warning

However, please note that the downloaded places need to be indexed after the installation and that this can take a considerable amount of time depending on the number of imported places but also the device's performance.

The Custom places search will be unavailable until the indexing is finished.

To check whether the indexing is finished, the SYCustomPlacesManagerDelegate has two callbacks that can be implemented:

class BasicCustomPlacesSearch: SYCustomPlacesManagerDelegate {}

extension BasicCustomPlacesSearch {
    func customPlacesManager(_ manager: SYCustomPlacesManager, didStartCreatingSearchIndexForDataset datasetId: String) {
        print("started indexing for dataset \(datasetId)")
    }

    func customPlacesManager(_ manager: SYCustomPlacesManager, didFinishCreatingSearchIndexForDataset datasetId: String, withError error: Error?) {
        print("finished indexing for dataset \(datasetId)")
    }
}

Note

The callbacks are sent for batches of indexing jobs and not for each country / JSON separately. That means that if two or more imports/installs finish at the same time and the indexing of the first one takes longer, the callback will be sent only after the indexing of the last one finishes.