import Foundation /// The CEP's coordinate, when the database has one. /// /// The point is the **median** of the coordinates of that CEP's addresses, not /// the mean: one badly geocoded address 40 km away shifts a mean and does not /// move a median. /// /// Always check ``precisao`` before using ``lat``/``lon`` for distance: /// /// - `"logradouro"`: the point is a stretch of street. ``raioM`` lands around /// 100–150 m. Good for maps and for distance. /// - `"localidade"`: the CEP *is* the whole municipality (a city-wide general /// CEP), and the point is its centroid. ``raioM`` runs from 7 km to 40 km. /// Computing shipping from this is wrong by tens of kilometres. /// /// ``raioM`` is the radius containing 90% of that CEP's addresses. It exists /// precisely so that error cannot pass unnoticed. /// /// The property names are the API's own, in Portuguese; only the casing is /// adapted, and ``raioM`` maps to the JSON's `raio_m`. public struct Geo: Codable, Sendable, Equatable, Hashable { /// Latitude in decimal degrees, WGS 84. public let lat: Double /// Longitude in decimal degrees, WGS 84. public let lon: Double /// Radius in metres containing 90% of the CEP's addresses. JSON `raio_m`. public let raioM: Int /// `"logradouro"` (a street) or `"localidade"` (a municipality centroid). public let precisao: String enum CodingKeys: String, CodingKey { case lat case lon case raioM = "raio_m" case precisao } public init(lat: Double, lon: Double, raioM: Int, precisao: String) { self.lat = lat self.lon = lon self.raioM = raioM self.precisao = precisao } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.lat = try container.decode(Double.self, forKey: .lat) self.lon = try container.decode(Double.self, forKey: .lon) if let exact = try? container.decode(Int.self, forKey: .raioM) { self.raioM = exact } else { self.raioM = Int(try container.decode(Double.self, forKey: .raioM)) } self.precisao = try container.decode(String.self, forKey: .precisao) } /// `true` when the coordinate points at a street, not a municipality. /// /// Shorthand for `precisao == "logradouro"`, which is the check to make /// before using the point to measure distance. public var isPrecise: Bool { precisao == "logradouro" } } /// A CEP and everything the database knows about it. /// /// Fields the database does not have for that CEP come back as empty strings, /// never `nil` — that is what the API returns, and changing it here would /// create a difference between reading the JSON and reading this library. /// /// ``geo`` is the exception: it is `nil` when there is no coordinate. There the /// difference is real, because the API **omits** the whole field in that case /// (about 43% of CEPs) rather than sending null. /// /// The property names are the API's own, in Portuguese. Everything else in /// this library is English; the fields are the one deliberate exception, so /// that what you read in the JSON at is exactly what /// you access on the value. public struct Address: Codable, Sendable, Equatable, Hashable { /// The CEP, always unmasked: `"01001000"`. See ``formattedCEP``. public let cep: String public let logradouro: String public let complemento: String public let unidade: String public let bairro: String public let localidade: String /// The two-letter state code: `"SP"`. public let uf: String public let estado: String public let regiao: String /// The IBGE municipality code, the key to join against census data. public let ibge: String public let gia: String public let ddd: String public let siafi: String /// The coordinate, or `nil` when the database has none for this CEP. /// /// Always `nil` for an ``Address`` obtained from /// ``Client/lookupMany(_:)`` — batches carry no coordinate at all. public let geo: Geo? enum CodingKeys: String, CodingKey { case cep, logradouro, complemento, unidade, bairro, localidade case uf, estado, regiao, ibge, gia, ddd, siafi, geo } public init( cep: String, logradouro: String = "", complemento: String = "", unidade: String = "", bairro: String = "", localidade: String = "", uf: String = "", estado: String = "", regiao: String = "", ibge: String = "", gia: String = "", ddd: String = "", siafi: String = "", geo: Geo? = nil ) { self.cep = cep self.logradouro = logradouro self.complemento = complemento self.unidade = unidade self.bairro = bairro self.localidade = localidade self.uf = uf self.estado = estado self.regiao = regiao self.ibge = ibge self.gia = gia self.ddd = ddd self.siafi = siafi self.geo = geo } /// Decodes the API's JSON object. /// /// Unknown fields are ignored, so a field the API grows before this library /// is updated cannot break an existing client. A malformed `geo` block /// yields `geo == nil` rather than failing the whole decode: it is not /// worth losing the address over, since the rest of the response is still /// useful. public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) func string(_ key: CodingKeys) -> String { (try? container.decode(String.self, forKey: key)) ?? "" } self.cep = string(.cep) self.logradouro = string(.logradouro) self.complemento = string(.complemento) self.unidade = string(.unidade) self.bairro = string(.bairro) self.localidade = string(.localidade) self.uf = string(.uf) self.estado = string(.estado) self.regiao = string(.regiao) self.ibge = string(.ibge) self.gia = string(.gia) self.ddd = string(.ddd) self.siafi = string(.siafi) self.geo = (try? container.decodeIfPresent(Geo.self, forKey: .geo)) ?? nil } /// The CEP with its mask: `"01001-000"`. /// /// The API always returns it unmasked; formatting is presentation, and it /// lives here so it does not become a repeated interpolation in every /// project that uses this library. public var formattedCEP: String { guard cep.count == 8 else { return cep } let split = cep.index(cep.startIndex, offsetBy: 5) return "\(cep[cep.startIndex..