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
|
package Cepis::Address;
use strict;
use warnings;
use Cepis::Geo;
our $VERSION = '0.2.0';
our @FIELDS = qw(
cep logradouro complemento unidade bairro localidade
uf estado regiao ibge gia ddd siafi
);
{
no strict 'refs';
for my $name (@FIELDS) {
*{$name} = sub { return $_[0]->{$name} };
}
}
sub from_json {
my ( $class, $data ) = @_;
my %raw = %{ $data || {} };
my %self;
for my $name (@FIELDS) {
$self{$name} = _string( $raw{$name} );
}
$self{geo} = Cepis::Geo->from_json( $raw{geo} );
$self{raw} = \%raw;
return bless \%self, $class;
}
sub geo { return $_[0]->{geo} }
sub raw { return $_[0]->{raw} }
sub field {
my ( $self, $name ) = @_;
return $self->{raw}{$name};
}
sub formatted_cep {
my ($self) = @_;
my $cep = $self->{cep};
return $cep unless length($cep) == 8;
return substr( $cep, 0, 5 ) . '-' . substr( $cep, 5, 3 );
}
sub to_hash {
my ($self) = @_;
my %out = map { $_ => $self->{$_} } @FIELDS;
$out{geo} = $self->{geo} ? $self->{geo}->to_hash : undef;
return \%out;
}
sub _string {
my ($value) = @_;
return '' unless defined $value;
return '' if ref $value;
return "$value";
}
1;
__END__
=encoding UTF-8
=head1 NAME
Cepis::Address - a CEP and everything the database knows about it
=head1 SYNOPSIS
my $a = Cepis::lookup('01310-100');
$a->logradouro; # "Avenida Paulista"
$a->localidade; # "São Paulo"
$a->uf; # "SP"
$a->ibge; # "3550308"
$a->cep; # "01310100" (always unmasked)
$a->formatted_cep; # "01310-100"
=head1 DESCRIPTION
B<Field names are the API's own, in Portuguese> — C<logradouro>, C<bairro>,
C<localidade>. Everything else in this library is English; the fields are the
one deliberate exception.
The reason is fidelity: what you read in the JSON at
L<https://cep.is/01001000> is exactly what you read on the object. Translating
to C<street>/C<city> would mean maintaining a field-by-field mapping and forcing
anyone reading the API docs alongside this library to translate back in their
head. 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
C<undef>. That is what the API returns, and changing it here would create a
difference between reading the JSON and reading this library.
L</geo> is the exception: it is C<undef> when there is no coordinate. There the
difference is real, because the API B<omits> the whole field in that case (about
43% of CEPs) rather than sending null.
=head1 CONSTRUCTOR
=head2 from_json
my $address = Cepis::Address->from_json( $hashref );
Builds from the decoded JSON object the API returned.
Unknown fields are ignored during construction but preserved in L</raw>. A new
field in the API therefore cannot break this library.
A malformed C<geo> block yields C<< geo => undef >> rather than propagating the
error.
=head1 ACCESSORS
=head2 cep
The 8 digits, unmasked.
=head2 logradouro
Street name.
=head2 complemento
Complement, such as C<"lado ímpar">.
=head2 unidade
Unit, when the CEP belongs to a specific building or post office box range.
=head2 bairro
Neighbourhood.
=head2 localidade
Municipality name.
=head2 uf
Two-letter state code.
=head2 estado
Full state name.
=head2 regiao
Region of the country, such as C<"Sudeste">.
=head2 ibge
IBGE municipality code.
=head2 gia
GIA code (São Paulo state tax registry).
=head2 ddd
Area code for telephone dialling.
=head2 siafi
SIAFI municipality code.
=head2 geo
A L<Cepis::Geo> object, or C<undef> when the database has no coordinate for
this CEP. Addresses coming out of C<lookup_many> always have C<undef> here — see
L<Cepis::Client/lookup_many>.
=head2 raw
The decoded response exactly as it arrived, as a hashref, so a field the API
grows before this library is updated is not lost.
=head2 field
$address->field('campo_do_futuro');
Reads any field of the raw response by name, including one this release does not
know about. Returns C<undef> when the field is absent.
=head2 formatted_cep
The CEP with its mask: C<01001-000>.
The API always returns it unmasked; formatting is presentation, and it lives
here so it does not become a repeated C<substr> in every project that uses this.
=head2 to_hash
Every named field plus C<geo> as a plain hashref, with C<geo> itself flattened
to a hashref or C<undef>.
=head1 SEE ALSO
L<Cepis>, L<Cepis::Geo>
=head1 LICENCE
ISC. Copyright (c) 2026 Murilo Ijanc E<lt>[email protected]<gt>.
=cut
|