How to localize an international address especially thoroughfare and subthoroughfare

1.2k views Asked by At

I'm working on an app that heavily uses addresses in different countries. We have a bunch of ways we input them from getting addresses imported, to dropping pins on map, to reverse geocode our current location.

My current project is to correctly format international address:

In the USA:

18 Street Name

In Norway:

Street Name 18

I've figured out a bunch of ways to instantiate CNMutablePostalAddress with CLPlacemark to get some pretty good results, the problem I'm having is this.

I want just the street name and number returned as a one line string: So: street1: placemarker.thoroughfare, (street name) street2: placemarker.subThoroughfare (street number),

but CNPostalAddress only has one street property, so to use it you need to do something like this:

cNPostalAddress.street = placemarker.subThoroughfare + " " + placemarker.thoroughfare

This will not work for countries like Norway where they are revered.

You can hack it and use the take the first line from the formatted address:

CNPostalAddressFormatter.string(from: placemarker.mailingAddress , style: .mailingAddress)

but that's super hacky and I'm sure it will break with countries that order their mailing address differently like japan.

At the moment I can't even find any resources that tell me which countries reverse subThoroughfare and thoroughfare, because if I had a list like that I could just reverse it manually.

Here is some sample code of what I've managed so far:

static func mulitLineAddress(from placemarker: CLPlacemark, detail: AddressDetail) -> String {
    let address = MailingAddress(
        street1: placemarker.thoroughfare,
        street2: placemarker.subThoroughfare,
        city: placemarker.locality,
        state: placemarker.administrativeArea,
        postalCode: placemarker.postalCode,
        countryCode: placemarker.country)

    return self.mulitLineAddress(from: address, detail: detail)
}


static func mulitLineAddress(from mailingAddress: MailingAddress, detail: AddressDetail) -> String {

let address = CNMutablePostalAddress()

let street1 = mailingAddress.street1 ?? ""
let street2 = mailingAddress.street2 ?? ""
let streetSpacing = street1.isEmpty && street2.isEmpty ? "" : " "
let streetFull = street1 + streetSpacing + street2

switch detail {
case .street1:
    address.street = street1
case .street2:
    address.street = street2
case .streetFull:
    address.street = streetFull
case .full:
    address.country = mailingAddress.countryCode ?? ""
    fallthrough
case .withoutCountry:
    address.street = streetFull
    address.city = mailingAddress.city ?? ""
    address.state = mailingAddress.state ?? ""
    address.postalCode = mailingAddress.postalCode ?? ""
}

return CNPostalAddressFormatter.string(from: address, style: .mailingAddress)
}

Any ideas? Even resources like list of countries that reverse street1 and street2 would be useful.

2

There are 2 answers

0
Jordan H On

CNPostalAddressFormatter has an interesting API to get an NSAttributedString where each address component is identified in it. You could use this to pull out just the information you want, like the street which includes the properly localized sub-thoroughfare and thoroughfare no matter where it may exist in the postal address.

let addressFormatter = CNPostalAddressFormatter()
let attributedAddress = addressFormatter.attributedString(from: postalAddress, withDefaultAttributes: [:])
let nsString = attributedAddress.string as NSString
let range = NSRange(location: 0, length: nsString.length)

var street: String?

attributedAddress.enumerateAttributes(in: range, options: []) { result, range, stop in
    if let component = result[NSAttributedString.Key(CNPostalAddressPropertyAttribute)] as? String {
        if component == CNPostalAddressStreetKey {
            street = nsString.substring(with: range)
            stop.pointee = true
        }
    }
}

I've also filed feedback with Apple to add a more powerful and flexible API: FB8648023 Template API desired to format addresses with specified components similar to DateFormatter.setLocalizedDateFormatFromTemplate

0
AdamAM On

I ended up taking a hybrid approach.

For the CLPlacemark to the CNMutablePostalAddress() it's pretty stright forward:

cnPostalAddress.street = placemarker.postalAddress?.street

However this doesn't work any any other input method and can't be modified to a different format from the CNMutablePostalAddress()

When bringing in address info from other sources I needed to do it manually, here is a bit of an example that works with a few countries:

static private func generateLocalizedStreetAddress(from adderss: MailingAddress) -> String {
    guard adderss.localizedStreet.isEmpty else { return adderss.localizedStreet ?? "" }

    let country = CountryCode.country(for: adderss.countryCode)
    let thoroughfare = adderss.thoroughfare ?? ""
    let subThoroughfare = adderss.subThoroughfare ?? ""
    let delimiter = self.generateDelimiter(from: thoroughfare, and: subThoroughfare, with: country)

    switch country {
    case .belgium, .czechRepublic, .denmark, .finland, .germany, .latvia, .netherlands, .norway, .poland, .portugal, .sweden:
        return thoroughfare + delimiter + subThoroughfare
    default:
        return subThoroughfare + delimiter + thoroughfare
    }
}

static private func generateDelimiter(from thoroughfare: String, and subThoroughfare: String, with country: Country) -> String {
    guard !thoroughfare.isEmpty && !subThoroughfare.isEmpty else { return "" }

    switch country {
    case .spain:
        return ", "
    default:
        return " "
    }
}