1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
|
package cepis
import (
"encoding/json"
"strconv"
)
// Geo is 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, in metres, containing 90% of that CEP's addresses. It
// exists precisely so that error cannot pass unnoticed.
type Geo struct {
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
RaioM int `json:"raio_m"`
Precisao string `json:"precisao"`
}
// IsPrecise reports whether the coordinate points at a street rather than at a
// whole municipality. Shorthand for Precisao == "logradouro", which is the
// check to make before using the point to measure distance.
func (g Geo) IsPrecise() bool {
return g.Precisao == "logradouro"
}
// Faixa is the municipality a CEP that does not exist would belong to, and the
// range of CEPs that municipality actually has. It arrives on the 404, hanging
// off [Error.Faixa].
//
// A CEP that fails is rarely a random number. It is usually someone who knows
// the city and guessed the rest, and the classic guess is the "-000": a small
// town often has a single city-wide CEP ending that way, but 957 municipalities
// are coded street by street and never got one. Areal/RJ answers from
// 25845-001 up, never at 25845-000 — so the 404 for 25845000 names Areal.
//
// CEPIni and CEPFim are the smallest and the largest CEP that EXIST in that
// municipality, not the bounds the Correios publish. Do not read the pair as a
// list of valid CEPs: the range is sparse, which is what CEPs is for — Areal's
// 118 CEPs are scattered across nearly five thousand numbers.
type Faixa struct {
Localidade string `json:"localidade"`
UF string `json:"uf"`
Estado string `json:"estado"`
Regiao string `json:"regiao"`
IBGE string `json:"ibge"`
CEPIni string `json:"cep_ini"`
CEPFim string `json:"cep_fim"`
// CEPs is how many CEPs the municipality actually has inside the range.
CEPs int `json:"ceps"`
}
// Address is a CEP and everything the database knows about it.
//
// The field names are the API's own, in Portuguese, with only the casing
// adapted to Go: what you read in the JSON at https://cep.is/01001000 is what
// you access here. The values are Portuguese either way — they come from the
// Correios database.
//
// Fields the database does not have for that CEP come back as empty strings,
// never null. Geo is the exception: it is nil when there is no coordinate,
// because the API omits the whole object in that case (about 43% of CEPs)
// rather than sending null.
//
// Raw holds the response exactly as it arrived, uninterpreted, so a field the
// API grows before this library is updated is not lost.
type Address struct {
CEP string `json:"cep"`
Logradouro string `json:"logradouro"`
Complemento string `json:"complemento"`
Unidade string `json:"unidade"`
Bairro string `json:"bairro"`
Localidade string `json:"localidade"`
UF string `json:"uf"`
Estado string `json:"estado"`
Regiao string `json:"regiao"`
IBGE string `json:"ibge"`
GIA string `json:"gia"`
DDD string `json:"ddd"`
Siafi string `json:"siafi"`
Geo *Geo `json:"geo,omitempty"`
// Raw is the response as it arrived, field by field, including fields
// this version of the library does not know about.
Raw map[string]json.RawMessage `json:"-"`
}
// FormattedCEP returns 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 Sprintf in every project.
func (a *Address) FormattedCEP() string {
if len(a.CEP) != 8 {
return a.CEP
}
return a.CEP[:5] + "-" + a.CEP[5:]
}
// Field returns a field of the raw response as a string, "" when the API did
// not send it. Use it to read a field the API grew after this library was
// released, without waiting for a new version.
func (a *Address) Field(name string) string {
if a.Raw == nil {
return ""
}
return rawString(a.Raw[name])
}
// UnmarshalJSON decodes the API's JSON leniently.
//
// Unknown fields are ignored during decoding but preserved in Raw, so a new
// field in the API cannot break this library. A malformed geo block yields a
// nil Geo rather than failing the whole document: the rest of the address is
// still useful and it is not worth losing over a coordinate.
func (a *Address) UnmarshalJSON(data []byte) error {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
*a = Address{
CEP: rawString(raw["cep"]),
Logradouro: rawString(raw["logradouro"]),
Complemento: rawString(raw["complemento"]),
Unidade: rawString(raw["unidade"]),
Bairro: rawString(raw["bairro"]),
Localidade: rawString(raw["localidade"]),
UF: rawString(raw["uf"]),
Estado: rawString(raw["estado"]),
Regiao: rawString(raw["regiao"]),
IBGE: rawString(raw["ibge"]),
GIA: rawString(raw["gia"]),
DDD: rawString(raw["ddd"]),
Siafi: rawString(raw["siafi"]),
Geo: decodeGeo(raw["geo"]),
Raw: raw,
}
return nil
}
func decodeGeo(block json.RawMessage) *Geo {
if len(block) == 0 {
return nil
}
var probe struct {
Lat *float64 `json:"lat"`
Lon *float64 `json:"lon"`
RaioM *int `json:"raio_m"`
Precisao *string `json:"precisao"`
}
if err := json.Unmarshal(block, &probe); err != nil {
return nil
}
if probe.Lat == nil || probe.Lon == nil || probe.RaioM == nil || probe.Precisao == nil {
return nil
}
return &Geo{
Lat: *probe.Lat,
Lon: *probe.Lon,
RaioM: *probe.RaioM,
Precisao: *probe.Precisao,
}
}
// decodeFaixa reads the `faixa` block of a 404 body. Every failure — absent
// block, malformed JSON, a `faixa` without a locality — yields nil: the field
// is an extra on top of an error that is already complete without it, and it
// must never turn a clean ErrCEPNotFound into a parse failure.
func decodeFaixa(body []byte) *Faixa {
if len(body) == 0 {
return nil
}
var envelope struct {
Faixa *Faixa `json:"faixa"`
}
if err := json.Unmarshal(body, &envelope); err != nil {
return nil
}
if envelope.Faixa == nil || envelope.Faixa.Localidade == "" {
return nil
}
return envelope.Faixa
}
func rawString(value json.RawMessage) string {
if len(value) == 0 {
return ""
}
var text string
if err := json.Unmarshal(value, &text); err == nil {
return text
}
var number json.Number
if err := json.Unmarshal(value, &number); err == nil {
return number.String()
}
return ""
}
// Health is the answer of GET /health.
//
// Good for two things: checking that the API answers, and knowing which
// version of the database is live. The DNE database changes every two weeks;
// DNE identifies it.
type Health struct {
Status string `json:"status"`
DNE string `json:"dne"`
CEPs int64 `json:"ceps"`
Geo int64 `json:"geo"`
BaseGeradaEm string `json:"base_gerada_em"`
// Raw is the response as it arrived, field by field.
Raw map[string]json.RawMessage `json:"-"`
}
// OK reports whether the API describes itself as healthy.
func (h *Health) OK() bool {
return h.Status == "ok"
}
// UnmarshalJSON decodes the health payload leniently, keeping the response in
// Raw and tolerating fields this version does not know about.
func (h *Health) UnmarshalJSON(data []byte) error {
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
*h = Health{
Status: rawString(raw["status"]),
DNE: rawString(raw["dne"]),
CEPs: rawInt(raw["ceps"]),
Geo: rawInt(raw["geo"]),
BaseGeradaEm: rawString(raw["base_gerada_em"]),
Raw: raw,
}
return nil
}
func rawInt(value json.RawMessage) int64 {
text := rawString(value)
if text == "" {
return 0
}
number, err := strconv.ParseFloat(text, 64)
if err != nil {
return 0
}
return int64(number)
}
|