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
|
(in-package #:cepis)
(defstruct (geo (:copier nil))
"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 `geo-precisao' before using the point for distance:
- \"logradouro\": the point is a stretch of street. `geo-raio-m' 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. `geo-raio-m' runs from 7 km to 40 km.
Computing shipping from this is wrong by tens of kilometres.
`geo-raio-m' is the radius containing 90% of that CEP's addresses. It exists
precisely so that error cannot pass unnoticed. The slot is the JSON's `raio_m'
with Lisp's hyphenation."
(lat 0.0d0 :type double-float :read-only t)
(lon 0.0d0 :type double-float :read-only t)
(raio-m 0 :type integer :read-only t)
(precisao "" :type string :read-only t))
(defun geo-precise-p (geo)
"True when the coordinate points at a street, not at a municipality.
Shorthand for (string= (geo-precisao geo) \"logradouro\"), which is the check
to make before using the point to measure distance."
(string= (geo-precisao geo) "logradouro"))
(defstruct (faixa (:copier nil))
"The municipality a CEP that does not exist would have belonged to.
Context for an error, never an address. It arrives on the 404 of a CEP the DNE
does not have, and names the municipality that owns the band the CEP falls in.
A CEP that fails is rarely random: it is usually someone who knows the city and
guessed the number — the classic guess being the `-000' that 957 municipalities
never received.
`faixa-cep-ini' and `faixa-cep-fim' are the smallest and the largest CEP that
*actually exist* in that municipality, not the bounds the Correios publish. For
Areal/RJ the DNE's own start is 25845000 — exactly the CEP that just failed —
while `faixa-cep-ini' is 25845001.
They are not the ends of a list, either: the range is sparse. Areal's 118 CEPs
are scattered over the ~4,900 numbers between its two ends, which is what
`faixa-ceps' counts. Offer the municipality to whoever typed the CEP; never
present the pair as a range of valid CEPs.
Both ends are strings, like every CEP in this library, because a leading zero
is part of the number."
(localidade "" :type string :read-only t)
(uf "" :type string :read-only t)
(estado "" :type string :read-only t)
(regiao "" :type string :read-only t)
(ibge "" :type string :read-only t)
(cep-ini "" :type string :read-only t)
(cep-fim "" :type string :read-only t)
(ceps 0 :type integer :read-only t))
(defstruct (address (:copier nil))
"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.
`address-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.
`address-raw' holds the response exactly as it arrived, as the hash table the
JSON parser produced, so a field the API grows before this library is updated
is not lost."
(cep "" :type string :read-only t)
(logradouro "" :type string :read-only t)
(complemento "" :type string :read-only t)
(unidade "" :type string :read-only t)
(bairro "" :type string :read-only t)
(localidade "" :type string :read-only t)
(uf "" :type string :read-only t)
(estado "" :type string :read-only t)
(regiao "" :type string :read-only t)
(ibge "" :type string :read-only t)
(gia "" :type string :read-only t)
(ddd "" :type string :read-only t)
(siafi "" :type string :read-only t)
(geo nil :type (or null geo) :read-only t)
(raw nil :type (or null hash-table) :read-only t))
(defun address-formatted-cep (address)
"The CEP with its mask: \"01001-000\".
The native contract always returns it unmasked; formatting is presentation,
and it lives here so it does not become a repeated format string in every
project that uses this."
(let ((cep (address-cep address)))
(if (= (length cep) 8)
(format nil "~a-~a" (subseq cep 0 5) (subseq cep 5))
cep)))
(defstruct (health (:copier nil))
"The response 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;
`health-dne' identifies it."
(status "" :type string :read-only t)
(dne "" :type string :read-only t)
(ceps 0 :type integer :read-only t)
(geo 0 :type integer :read-only t)
(base-gerada-em "" :type string :read-only t))
(defun health-ok-p (health)
"True when the API reports itself healthy."
(string= (health-status health) "ok"))
(defun json-string (table key)
"The value of KEY in TABLE as a string, or \"\" when it is absent or not a scalar."
(let ((value (gethash key table)))
(cond ((stringp value) value)
((integerp value) (format nil "~d" value))
((realp value) (princ-to-string value))
(t ""))))
(defun json-integer (table key)
"The value of KEY in TABLE as an integer, or 0 when it is absent or not a number."
(let ((value (gethash key table)))
(if (realp value) (round value) 0)))
(defun geo-from-json (value)
"Build a `geo' from the parsed \"geo\" block, or NIL when it is absent or malformed.
A malformed block yields NIL rather than propagating the error: it is not
worth losing the whole address over, since the rest of the response is still
useful."
(when (hash-table-p value)
(let ((lat (gethash "lat" value))
(lon (gethash "lon" value))
(raio-m (gethash "raio_m" value))
(precisao (gethash "precisao" value)))
(when (and (realp lat) (realp lon) (realp raio-m) (stringp precisao))
(make-geo :lat (coerce lat 'double-float)
:lon (coerce lon 'double-float)
:raio-m (round raio-m)
:precisao precisao)))))
(defun faixa-from-json (value)
"Build a `faixa' from the parsed \"faixa\" block, or NIL when it is absent or
malformed.
The API omits the field entirely — it never sends null — when the CEP falls in
a band no municipality owns, so an absent block is the ordinary case and not an
anomaly.
Anything that is not a block naming a municipality also yields NIL. That is the
whole point of parsing it here and not at the call site: the response is a
definitive not-found, and a field that only adds context must never be able to
turn it into a parse failure."
(when (hash-table-p value)
(let ((localidade (json-string value "localidade")))
(when (plusp (length localidade))
(make-faixa :localidade localidade
:uf (json-string value "uf")
:estado (json-string value "estado")
:regiao (json-string value "regiao")
:ibge (json-string value "ibge")
:cep-ini (json-string value "cep_ini")
:cep-fim (json-string value "cep_fim")
:ceps (json-integer value "ceps"))))))
(defun address-from-json (data)
"Build an `address' from the hash table the JSON parser produced.
Unknown fields are ignored during construction but preserved in
`address-raw'. A new field in the API therefore cannot break this library."
(make-address :cep (json-string data "cep")
:logradouro (json-string data "logradouro")
:complemento (json-string data "complemento")
:unidade (json-string data "unidade")
:bairro (json-string data "bairro")
:localidade (json-string data "localidade")
:uf (json-string data "uf")
:estado (json-string data "estado")
:regiao (json-string data "regiao")
:ibge (json-string data "ibge")
:gia (json-string data "gia")
:ddd (json-string data "ddd")
:siafi (json-string data "siafi")
:geo (geo-from-json (gethash "geo" data))
:raw data))
(defun health-from-json (data)
"Build a `health' from the hash table the JSON parser produced."
(make-health :status (json-string data "status")
:dne (json-string data "dne")
:ceps (json-integer data "ceps")
:geo (json-integer data "geo")
:base-gerada-em (json-string data "base_gerada_em")))
|