summaryrefslogtreecommitdiffstats
path: root/src/Cepis.pas
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-27 20:42:22 -0300
committerMurilo Ijanc <[email protected]>2026-07-27 20:42:22 -0300
commitb6f876f8b0847c223f3310d3e5208d78982a13c1 (patch)
treefce85d75a2e491b6514a4ed7c42dca898f944cc5 /src/Cepis.pas
parentcf5597871327dbff2c7c743672d696e7ff340c42 (diff)
downloadpascal-6637db2cb939c538c039b501ba9d827f07545208.tar.gz
Carry the municipality range on a not-found CEPHEADv0.2.0main
A CEP that fails is rarely a random number: it is usually someone who knows the city and guessed the last digits, and the classic guess is the -000 that 957 municipalities never received. The API now answers those 404s with a faixa block naming the municipality the number would have belonged to, which is enough to tell the user "no such CEP in Areal" instead of just "not found". It stays an error. ECepisNotFound carries the block as a TCepisFaixa and owns it, the same way an address owns its geo, so no shape of the response can turn a not-found into an address. Parsing is tolerant on purpose: a faixa that is absent, null, empty or unreadable leaves Faixa nil rather than costing the caller the clean not-found underneath. CepIni and CepFim are the first and the last CEP that actually exist in the municipality - not the bounds the Correios publish, whose start for Areal is the very CEP that just failed - and the range between them is sparse. That is what Ceps is for, and why the comments insist the pair is not a list.
Diffstat (limited to 'src/Cepis.pas')
-rw-r--r--src/Cepis.pas232
1 files changed, 227 insertions, 5 deletions
diff --git a/src/Cepis.pas b/src/Cepis.pas
index 7b628e9..cf62d08 100644
--- a/src/Cepis.pas
+++ b/src/Cepis.pas
@@ -16,7 +16,8 @@
Ownership, in one sentence: every object this unit hands back belongs to the
caller, who frees it - except the geo block, which belongs to its address,
- and the default client, which belongs to this unit.
+ the faixa block, which belongs to the not-found error carrying it, and the
+ default client, which belongs to this unit.
Note on field names: the API surface is English, but the data fields keep the
API's own Portuguese names (Logradouro, Bairro, Localidade). What you read in
@@ -36,7 +37,7 @@ uses
const
{ Version of this library, as it appears in the default User-Agent. }
- CepisVersion = '0.1.0';
+ CepisVersion = '0.2.0';
{ Where the API lives when nothing else is configured. }
CepisDefaultURL = 'https://cep.is';
@@ -45,6 +46,9 @@ const
CepisMaxBatch = 500;
type
+ { Declared in full below. ECepisNotFound carries one. }
+ TCepisFaixa = class;
+
{
Base of every error this unit raises. Catch it to handle any failure.
@@ -71,8 +75,48 @@ type
{
The CEP is well formed but does not exist in the DNE database.
+
+ Faixa, when it is there, names the municipality whose CEP band the number
+ falls into - enough to answer "that CEP does not exist in Areal" instead of
+ just "not found". It is context for the failure and nothing more: the CEP
+ asked for still does not exist, and a faixa never turns this into an
+ address.
+
+ It is nil when the number falls in a band no municipality owns, and nil
+ against an API that does not send the field at all. The API omits it rather
+ than sending null, so there is no third state to handle. HasFaixa is the
+ shorthand for the check.
}
- ECepisNotFound = class(ECepis);
+ ECepisNotFound = class(ECepis)
+ private
+ FFaixa: TCepisFaixa;
+ function GetHasFaixa: Boolean;
+ public
+ { Takes ownership of AFaixa, which may be nil. }
+ constructor Create(const AMessage: string; AFaixa: TCepisFaixa);
+ reintroduce;
+
+ { Frees the faixa, if any. }
+ destructor Destroy; override;
+
+ {
+ Hand the faixa over to the caller, who becomes responsible for freeing
+ it. Faixa is nil afterwards. Returns nil when there was none.
+
+ The exception itself is freed when the except block ends, so this is the
+ way to let the faixa outlive the handler.
+ }
+ function ExtractFaixa: TCepisFaixa;
+
+ { True when the API placed the CEP inside a municipality's band. }
+ property HasFaixa: Boolean read GetHasFaixa;
+
+ {
+ The municipality the CEP would belong to, or nil. Owned by this
+ exception; see ExtractFaixa.
+ }
+ property Faixa: TCepisFaixa read FFaixa;
+ end;
{
The request never completed: DNS, refused connection, timeout, TLS.
@@ -174,6 +218,73 @@ type
end;
{
+ The municipality a CEP that does not exist would have belonged to.
+
+ Attached to ECepisNotFound when the failed number falls inside the band the
+ Correios allocated to one municipality. The lookup still failed - this is
+ context for the error, never an address.
+
+ Why the API bothers: a CEP that fails is rarely a random number. It is
+ usually someone who knows the city and guessed the last digits, and the
+ classic guess is the '-000' that 957 municipalities never received.
+
+ CepIni and CepFim are the smallest and the largest CEP that really exist
+ there, not the bounds the Correios publish. Areal's DNE band opens at
+ 25845000 - which is exactly the CEP that just failed - while its CepIni is
+ 25845001.
+
+ The pair is not a list. The range is sparse: Areal's 118 CEPs are scattered
+ across the ~4,900 numbers between 25845001 and 25849899, so iterating from
+ one bound to the other yields forty times more numbers than exist. Ceps is
+ how many there are, and it is there precisely so the gap cannot pass
+ unnoticed.
+
+ Owned by the ECepisNotFound that carries it, and freed with it. Use
+ ECepisNotFound.ExtractFaixa to take ownership.
+ }
+ TCepisFaixa = class(TPersistent)
+ private
+ FLocalidade: string;
+ FUF: string;
+ FEstado: string;
+ FRegiao: string;
+ FIbge: string;
+ FCepIni: string;
+ FCepFim: string;
+ FCeps: Int64;
+ public
+ constructor Create(const ALocalidade, AUF, AEstado, ARegiao, AIbge,
+ ACepIni, ACepFim: string; ACeps: Int64);
+ published
+ { Municipality that owns the band, e.g. 'Areal'. }
+ property Localidade: string read FLocalidade;
+
+ { Two-letter state code, e.g. 'RJ'. }
+ property UF: string read FUF;
+
+ { State name in full, e.g. 'Rio de Janeiro'. }
+ property Estado: string read FEstado;
+
+ { Region of the country, e.g. 'Sudeste'. }
+ property Regiao: string read FRegiao;
+
+ { IBGE municipality code - the key to join against census data. }
+ property Ibge: string read FIbge;
+
+ { Smallest CEP that exists in the municipality, unmasked. }
+ property CepIni: string read FCepIni;
+
+ { Largest CEP that exists in the municipality, unmasked. }
+ property CepFim: string read FCepFim;
+
+ {
+ How many CEPs exist between CepIni and CepFim, both included. Far fewer
+ than the arithmetic distance between them - see the note above.
+ }
+ property Ceps: Int64 read FCeps;
+ end;
+
+ {
A CEP and everything the database knows about it.
Fields the database does not have for that CEP come back as empty strings,
@@ -398,6 +509,10 @@ type
database; ECepisRateLimited, ECepisServerError or ECepisNetworkError once
the attempts are exhausted; ECepisInvalidResponse when a success status
carries a body that is not a JSON object.
+
+ The ECepisNotFound may carry a TCepisFaixa naming the municipality the
+ CEP would have belonged to. Read it there; a not-found is still a
+ not-found.
}
function Lookup(const ACep: string): TCepisAddress;
@@ -783,6 +898,76 @@ begin
Result := TCepisGeo.Create(Lat, Lon, RaioM, Precisao);
end;
+{
+ Read the faixa block of a 404 body, or nil when there is none to read.
+
+ Deliberately tolerant. A body without 'faixa', with a null or a string
+ there, with an empty object, or with nothing that names a municipality all
+ give nil - which is the same thing the caller sees against an API that never
+ sends the field. A field that is there but unreadable falls back to empty,
+ or to 0 for Ceps, rather than throwing the municipality away with it.
+
+ Nothing here raises and nothing here leaks: a faixa is a bonus on top of an
+ error, and no shape of it may cost the caller the clean not-found underneath.
+}
+function FaixaFromJSON(AObject: TJSONObject): TCepisFaixa;
+var
+ Block: TJSONData;
+ Faixa: TJSONObject;
+ Localidade: string;
+begin
+ Result := nil;
+ if AObject = nil then
+ Exit;
+ Block := AObject.Find('faixa');
+ if not (Block is TJSONObject) then
+ Exit;
+ Faixa := TJSONObject(Block);
+ Localidade := JSONString(Faixa, 'localidade');
+ if Localidade = '' then
+ Exit;
+ Result := TCepisFaixa.Create(Localidade,
+ JSONString(Faixa, 'uf'),
+ JSONString(Faixa, 'estado'),
+ JSONString(Faixa, 'regiao'),
+ JSONString(Faixa, 'ibge'),
+ JSONString(Faixa, 'cep_ini'),
+ JSONString(Faixa, 'cep_fim'),
+ JSONInt64(Faixa, 'ceps'));
+end;
+
+{
+ Build the ECepisNotFound for a 404, faixa included when the body carries one.
+
+ The exception is returned rather than raised so the faixa has exactly one
+ owner from the moment it exists.
+}
+function NotFoundFor(const ANumber, ABody: string): ECepisNotFound;
+var
+ Data: TJSONData;
+ Faixa: TCepisFaixa;
+ Text: string;
+begin
+ Faixa := nil;
+ Data := ParseJSONLoose(ABody);
+ try
+ if Data is TJSONObject then
+ Faixa := FaixaFromJSON(TJSONObject(Data));
+ finally
+ Data.Free;
+ end;
+
+ Text := 'CEP ' + ANumber + ' does not exist in the DNE database';
+ if Faixa <> nil then
+ begin
+ Text := Text + ' - it falls in the CEP range of ' + Faixa.Localidade;
+ if Faixa.UF <> '' then
+ Text := Text + '/' + Faixa.UF;
+ end;
+
+ Result := ECepisNotFound.Create(Text, Faixa);
+end;
+
function AddressFromJSON(AObject: TJSONObject; const ARaw: string): TCepisAddress;
begin
Result := TCepisAddress.Create;
@@ -837,6 +1022,44 @@ begin
FStatus := AStatus;
end;
+constructor ECepisNotFound.Create(const AMessage: string;
+ AFaixa: TCepisFaixa);
+begin
+ inherited Create(AMessage);
+ FFaixa := AFaixa;
+end;
+
+destructor ECepisNotFound.Destroy;
+begin
+ FreeAndNil(FFaixa);
+ inherited Destroy;
+end;
+
+function ECepisNotFound.ExtractFaixa: TCepisFaixa;
+begin
+ Result := FFaixa;
+ FFaixa := nil;
+end;
+
+function ECepisNotFound.GetHasFaixa: Boolean;
+begin
+ Result := FFaixa <> nil;
+end;
+
+constructor TCepisFaixa.Create(const ALocalidade, AUF, AEstado, ARegiao,
+ AIbge, ACepIni, ACepFim: string; ACeps: Int64);
+begin
+ inherited Create;
+ FLocalidade := ALocalidade;
+ FUF := AUF;
+ FEstado := AEstado;
+ FRegiao := ARegiao;
+ FIbge := AIbge;
+ FCepIni := ACepIni;
+ FCepFim := ACepFim;
+ FCeps := ACeps;
+end;
+
constructor TCepisGeo.Create(ALat, ALon: Double; ARaioM: Integer;
const APrecisao: string);
begin
@@ -1072,8 +1295,7 @@ begin
Request('/' + Number, 0, Status, Body);
if Status = 404 then
- raise ECepisNotFound.Create('CEP ' + Number +
- ' does not exist in the DNE database');
+ raise NotFoundFor(Number, Body);
if Status = 400 then
raise ECepisInvalidCEP.Create(ErrorMessageOf(Body,
'CEP ' + Number + ' rejected'));