package Cepis::Faixa; use strict; use warnings; use Scalar::Util (); our $VERSION = '0.2.0'; sub from_json { my ( $class, $data ) = @_; return undef unless ref $data eq 'HASH'; for my $key (qw(localidade cep_ini cep_fim ceps)) { return undef unless defined $data->{$key}; return undef if ref $data->{$key}; } return undef unless length "$data->{localidade}"; for my $key (qw(cep_ini cep_fim)) { return undef unless "$data->{$key}" =~ /\A[0-9]{8}\z/; } return undef unless Scalar::Util::looks_like_number( $data->{ceps} ); return bless { localidade => "$data->{localidade}", uf => _string( $data->{uf} ), estado => _string( $data->{estado} ), regiao => _string( $data->{regiao} ), ibge => _string( $data->{ibge} ), cep_ini => "$data->{cep_ini}", cep_fim => "$data->{cep_fim}", ceps => int( 0 + $data->{ceps} ), }, $class; } sub localidade { return $_[0]->{localidade} } sub uf { return $_[0]->{uf} } sub estado { return $_[0]->{estado} } sub regiao { return $_[0]->{regiao} } sub ibge { return $_[0]->{ibge} } sub cep_ini { return $_[0]->{cep_ini} } sub cep_fim { return $_[0]->{cep_fim} } sub ceps { return $_[0]->{ceps} } sub label { my ($self) = @_; return length $self->{uf} ? "$self->{localidade}/$self->{uf}" : $self->{localidade}; } sub to_hash { my ($self) = @_; return { localidade => $self->{localidade}, uf => $self->{uf}, estado => $self->{estado}, regiao => $self->{regiao}, ibge => $self->{ibge}, cep_ini => $self->{cep_ini}, cep_fim => $self->{cep_fim}, ceps => $self->{ceps}, }; } sub _string { my ($value) = @_; return '' unless defined $value; return '' if ref $value; return "$value"; } 1; __END__ =encoding UTF-8 =head1 NAME Cepis::Faixa - the municipality a CEP that does not exist would belong to =head1 SYNOPSIS my $address = eval { Cepis::lookup('25845000') }; if ( !$address && ref $@ && $@->isa('Cepis::Error::CEPNotFound') ) { if ( my $faixa = $@->faixa ) { print $faixa->localidade, "\n"; # "Areal" print $faixa->uf, "\n"; # "RJ" print $faixa->ceps, "\n"; # 118 printf "%s..%s\n", $faixa->cep_ini, $faixa->cep_fim; # 25845001..25849899 } } =head1 DESCRIPTION B The lookup still failed, the CEP still does not exist, and L still throws L. What this object adds is I — enough to tell the user "25845000 does not exist in Areal/RJ" instead of "not found". That matters because a failing CEP is rarely random: it is usually someone who knows the city and guessed the number, and the classic guess is the C<-000> that 957 municipalities never received. The object is reachable as C<< $err->faixa >> on L, and is C whenever the API sent no C — the API B the field rather than sending null when the CEP falls in a band no municipality owns. =head2 The range is not a list of valid CEPs L and L are the B in that municipality. They are not the bounds the Correios publish: the DNE's own start for Areal is C<25845000> — exactly the CEP that just failed. The range is also B. Areal's 118 CEPs are scattered across the roughly 4,900 numbers between C<25845001> and C<25849899>. Iterating the interval, or presenting it as "the valid CEPs for this city", produces mostly numbers that do not exist. L is there precisely so that cannot pass unnoticed: it says how many of them are real. Field names are the API's own, in Portuguese. See L for why. =head1 CONSTRUCTOR =head2 from_json my $faixa = Cepis::Faixa->from_json( $hashref ); Builds from the C block of a 404 body. Returns C when the block is missing, empty, not a hash, or malformed. Parsing here is deliberately forgiving: a bad C must never turn a clean not-found into a parse failure. The caller still gets its L, just without the extra context. =head1 ACCESSORS =head2 localidade Municipality name. Always present and non-empty — a block without it does not build. =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. The stable way to identify the municipality; the name alone is not unique across states. =head2 cep_ini The smallest CEP that exists in the municipality, unmasked. Read the warning above before treating it as a lower bound. =head2 cep_fim The largest CEP that exists in the municipality, unmasked. =head2 ceps How many CEPs the municipality actually has, as an integer. Compare it with the span between L and L to see how sparse the range is. =head2 label C<"Areal/RJ">: the municipality and its state, ready to put in a message. Falls back to the municipality alone when the API sent no C. =head2 to_hash The eight fields as a plain hashref. =head1 SEE ALSO L, L, L =head1 LICENCE ISC. Copyright (c) 2026 Murilo Ijanc Emurilo@ijanc.orgE. =cut