-->

CLGeocoder returns wrong results when city name is

2020-02-10 13:29发布

问题:

In one of my apps I need to add an ability to find a city by its name. I am using CLGeocoder to achieve this and I want it to have a behaviour identical to iOS weather app.

Below is the code which I have:

CLGeocoder().geocodeAddressString(searchBar.text!, completionHandler:{ (placemarks, error) -> Void in
    guard let nonNilMarks = placemarks else {return}
    for placemark in nonNilMarks {
        print("locality: \(placemark.locality)")
        print("name: \(placemark.name)")
        print("country: \(placemark.country)")
        print("formatted address: \(placemark.addressDictionary)")
    }
})

It works very well in most cases. However, I recently noticed some cases when it fails. Below is the output for some examples:

Searching for 'Milan' - WORKS

locality: Optional("Milan")
name: Optional("Milan")
country: Optional("Italy")
formatted address: Optional([SubAdministrativeArea: Milan, State: Lombardy, CountryCode: IT, Country: Italy, Name: Milan, FormattedAddressLines: (
    Milan,
    Italy
), City: Milan])

This is the correct output

Searching for 'Italy, TX' - WORKS

There is a town in Texas called Italy. If I type 'Italy, TX' the output is:

locality: Optional("Italy")
name: Optional("Italy")
country: Optional("United States")
formatted address: Optional([SubAdministrativeArea: Ellis, State: TX, CountryCode: US, Country: United States, Name: Italy, FormattedAddressLines: (
    "Italy, TX",
    "United States"
), City: Italy])

Searching for 'Italy' - FAILS

When I type just 'Italy' I only get place mark for the country:

locality: nil
name: Optional("Italy")
country: Optional("Italy")
formatted address: Optional([CountryCode: IT, Name: Italy, FormattedAddressLines: (
    Italy
), Country: Italy])

Searching for 'Singapore, Singapore' - WORKS

This is similar to the case with 'Italy, TX':

locality: Optional("Singapore")
name: Optional("Singapore")
country: Optional("Singapore")
formatted address: Optional([City: Singapore, CountryCode: SG, Name: Singapore, State: Singapore, FormattedAddressLines: (
    Singapore,
    Singapore
), Country: Singapore])

Searching for 'Singapore' - FAILS

Again, seems similar to the case with 'Italy'. It only finds a country:

locality: nil
name: Optional("Singapore")
country: Optional("Singapore")
formatted address: Optional([CountryCode: SG, Name: Singapore, FormattedAddressLines: (
    Singapore
), Country: Singapore])

Preliminary guess

At this stage I though that maybe geocodeAddressString: stops searching if it finds a country with the name equal to the search parameter, so I tried changing my code to :

let addrDict = [kABPersonAddressCityKey as NSString: searchBar.text!]
CLGeocoder().geocodeAddressDictionary(addrDict, completionHandler:{ (placemarks, error) -> Void in
    print(placemarks)
})

I thought that by restricting the search term to the city name I would get correct results. Unfortunately, I get the exact same behaviour!

And THEN I realised that I have problems not only with cases when city name is equal to the country name.

Searching for 'Tokyo' - EPIC FAIL

locality: nil
name: Optional("Tokyo")
country: Optional("Japan")
formatted address: Optional([CountryCode: JP, Name: Tokyo, State: Tokyo, FormattedAddressLines: (
    Tokyo,
    Japan
), Country: Japan])

Conclusion

Not only can CLGeocoder omit results (like in the case of 'Italy') but it can also tell me that a place does not have a locality when, in fact, it does (I believe last time I checked a map Tokyo was still a city!).

Does anyone know how I can resolve any of these issues?

Weather app does it somehow - searching for 'Singapore' or 'Italy' there gives correct results. I will be grateful for any help!

P.S. Although I am using Swift I am adding Objective-C as a tag as I think that this question is not Swift specific.

回答1:

Usage what you showed is completely right and I don't think there is anything else you can do to improve results. It is still just some complicated algorithm that tries to sort results based on set of rules and some assumption.

That being sad, I understand your frustration completely because I've been there and done that. The problem is that Geocoding database is not obviously complete and Apple is not the company who leads this field - Google is. Here you can see technical note from when the Geocoder was introduced saying that there are still countries that are not supported, or just partially supported.

So my suggestion to you is that if you want to get best results (though still not fail-proof), switch to Google Geolocation. For quite great amount of requests it is free and after that it costs some very minor amounts. The success rate is around 99% for all the basic searches from what I experienced and it only gets better if you provide more information. The API options are also quite extensive.

Here are links for developer documentation for both Geocoding and Reverse Geocoding: https://developers.google.com/maps/documentation/geocoding/intro https://developers.google.com/maps/documentation/javascript/examples/geocoding-reverse

Hope it helped at least a little.

EDIT: After I wrote this I did a little research and it seems that I am not the only person to make this claim (also including the same unsupported link).



回答2:

CLGeocoder requests are rate limited and if the app goes over the threshold, further requests may not be fulfilled. CLGeocoder requires network connection which may be limiting under some circumstances and the response from the server is not always instant. If you do not insist on CLGeocoder, local database might give you more flexibility and possibly better user experience.

Take a look at the cities???.zip on GeoNames, for an alternative solution where you would import the data to a local database.



回答3:

You can also do this by google API like this

//chakshu

       var urlString = "https://maps.googleapis.com/maps/api/place/autocomplete/json?input=\(searchString)&sensor=true&key=\(Google_Browser_Key)"

                var linkUrl:NSURL = NSURL(string:urlString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)!)!

                if(countElements(urlString)>0 && !urlString.isEmpty)
                {
                   // if let fileData = String(contentsOfURL: NSURL(string: urlString)!, encoding: NSUTF8StringEncoding, error: nil)
                    if let fileData = String(contentsOfURL: linkUrl, encoding: NSUTF8StringEncoding, error: nil)
                    {
                        println(fileData)

                        var data = fileData.dataUsingEncoding(NSASCIIStringEncoding, allowLossyConversion: false)
                        var localError: NSError?
                        if(data != nil)
                        {
                            var json: AnyObject! = NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers, error: &localError)

}
}

}


回答4:

I was facing this problem too here on Brazil, só I made a terrible solution for that:

public func getLatLonFrom(address: String, completion:@escaping ((CLLocation?, NSError?)->Void)) {
        //LCEssentials.printInfo(title: "LOCATION", msg: "CEP: \(address)")
        let geoCoder = CLGeocoder()
        let params: [String: Any] = [
            CNPostalAddressPostalCodeKey: address,
            CNPostalAddressISOCountryCodeKey: "BR"
        ]
        geoCoder.geocodeAddressString(address) { (placemarks, error) in
            //LCEssentials.printInfo(title: "LOCATION", msg: "PLACEMARKS FIRST: \(placemarks?.debugDescription)")
            if let locais = placemarks {
                guard let location = locais.first?.location else {
                    let error = NSError(domain: LCEssentials().DEFAULT_ERROR_DOMAIN, code: LCEssentials().DEFAULT_ERROR_CODE, userInfo: [ NSLocalizedDescriptionKey: "Address not found" ])
                    completion(nil, error)
                    return
                }
                completion(location, nil)
            }else{
                geoCoder.geocodeAddressDictionary(params) { (placemarks, error) in
                    //LCEssentials.printInfo(title: "LOCATION", msg: "PLACEMARKS: \(placemarks?.debugDescription)")
                    guard let founded = placemarks, let location = founded.first?.location else {
                        let error = NSError(domain: LCEssentials().DEFAULT_ERROR_DOMAIN, code: LCEssentials().DEFAULT_ERROR_CODE, userInfo: [ NSLocalizedDescriptionKey: "Address not found" ])
                        completion(nil, error)
                        return
                    }
                    completion(location, nil)
                }
            }
        }
    }

I force the Country Code to BR ( Brazil ) and then i can find the address by ZIP CODE. If it return NIL for placemarks on geocodeAddressString then i call the geocodeAddressDictionary and i receive the correct result.
This solution can cover major zip code from BR. It is a mess code, but for now it works.
Future projects i will use Google Maps.