package Cepis::Address; use strict; use warnings; use Cepis::Geo; our $VERSION = '0.1.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 — C, C, C. 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 is exactly what you read on the object. Translating to C/C 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. That is what the API returns, and changing it here would create a difference between reading the JSON and reading this library. L is the exception: it is C when there is no coordinate. There the difference is real, because the API B 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. A new field in the API therefore cannot break this library. A malformed C 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 object, or C when the database has no coordinate for this CEP. Addresses coming out of C always have C here — see L. =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 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 in every project that uses this. =head2 to_hash Every named field plus C as a plain hashref, with C itself flattened to a hashref or C. =head1 SEE ALSO L, L =head1 LICENCE ISC. Copyright (c) 2026 Murilo Ijanc Emurilo@ijanc.orgE. =cut