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)
            """)
    }
}