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
|
package Cepis::Geo;
use strict;
use warnings;
use Scalar::Util ();
our $VERSION = '0.1.0';
sub from_json {
my ( $class, $data ) = @_;
return undef unless ref $data eq 'HASH';
for my $key (qw(lat lon raio_m precisao)) {
return undef unless defined $data->{$key};
}
for my $key (qw(lat lon raio_m)) {
return undef unless Scalar::Util::looks_like_number( $data->{$key} );
}
return undef if ref $data->{precisao};
return bless {
lat => 0 + $data->{lat},
lon => 0 + $data->{lon},
raio_m => int( 0 + $data->{raio_m} ),
precisao => "$data->{precisao}",
}, $class;
}
sub lat { return $_[0]->{lat} }
sub lon { return $_[0]->{lon} }
sub raio_m { return $_[0]->{raio_m} }
sub precisao { return $_[0]->{precisao} }
sub is_precise { return $_[0]->{precisao} eq 'logradouro' ? 1 : 0 }
sub to_hash {
my ($self) = @_;
return {
lat => $self->{lat},
lon => $self->{lon},
raio_m => $self->{raio_m},
precisao => $self->{precisao},
};
}
1;
__END__
=encoding UTF-8
=head1 NAME
Cepis::Geo - the coordinate of a CEP
=head1 SYNOPSIS
my $address = Cepis::lookup('01310100');
if ( my $geo = $address->geo ) {
printf "%s %s\n", $geo->lat, $geo->lon; # -23.567202 -46.649659
print $geo->raio_m, "\n"; # 1084
print $geo->precisao, "\n"; # logradouro
}
=head1 DESCRIPTION
The CEP's coordinate, when the database has one — about 57% of them. When there
is none, C<< $address->geo >> is C<undef>; the API omits the whole block rather
than sending null.
The point is the B<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.
B<Always check C<precisao> before using C<lat>/C<lon> for distance.> There are
two kinds of point and the difference between them is tens of kilometres:
=over 4
=item C<"logradouro">
The point is a stretch of street. C<raio_m> lands around 100–150 m. Good for
maps and for distance.
=item C<"localidade">
The CEP I<is> the whole municipality (a city-wide general CEP), and the point is
its centroid. C<raio_m> runs from 7 km to 40 km. Computing shipping from this is
wrong by tens of kilometres.
=back
C<raio_m> is the radius containing 90% of that CEP's addresses. It exists
precisely so that error cannot pass unnoticed.
Field names are the API's own, in Portuguese. See L<Cepis::Address> for why.
=head1 CONSTRUCTOR
=head2 from_json
my $geo = Cepis::Geo->from_json( $hashref );
Builds from the C<geo> block the API returned. Returns C<undef> when the block
is missing, is not a hash, or is malformed — a bad C<geo> is not worth losing
the whole address over, since the rest of the response is still useful.
=head1 ACCESSORS
=head2 lat
Latitude, as a number.
=head2 lon
Longitude, as a number.
=head2 raio_m
Radius in metres containing 90% of that CEP's addresses, as an integer.
=head2 precisao
Either C<"logradouro"> or C<"localidade">. Read the warning above before using
the coordinate for anything metric.
=head2 is_precise
True when the coordinate points at a street rather than a municipality.
Shorthand for C<< $geo->precisao eq 'logradouro' >>, which is the check to make
before using the point to measure distance.
=head2 to_hash
The four fields as a plain hashref.
=head1 SEE ALSO
L<Cepis>, L<Cepis::Address>
=head1 LICENCE
ISC. Copyright (c) 2026 Murilo Ijanc E<lt>[email protected]<gt>.
=cut
|