summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-27 20:38:46 -0300
committerMurilo Ijanc <[email protected]>2026-07-27 20:38:46 -0300
commit3506a69f15fdf1c1c80acec26e906e6f25eeff14 (patch)
tree626ddce348a635e91aa7a5d3f64f39e2ba374841
parentef1e5586a47002a6f21f43871b5fcfdb2c60af5d (diff)
downloadfortran-0.2.0.tar.gz
Name the municipality behind a CEP that does not existHEADv0.2.0main
A CEP that fails is rarely a random number. It is usually someone who knows the city and guessed the digits, and the classic guess is the -000 that 957 municipalities never received. Answering that with a bare "not found" throws away what the API already knows: which municipality the number would have fallen under. The 404 now carries an optional faixa, and it is exposed the way geo already is — a derived type behind a logical flag on the address, because the API omits the block entirely rather than sending null. It stays a 404: found is still false and the status is still CEPIS_ERR_NOT_FOUND. The band is context for the failure, never an address. cep_ini and cep_fim are the smallest and largest CEP that actually exist there, not the bounds the Correios publish — the published start for Areal is 25845000, exactly the CEP that fails. The pair is not a list either: 118 CEPs over some 4,900 numbers, which is what ceps is for. Parsing is deliberately silent about every failure. A body that is not JSON, or a faixa that is absent, null, not an object or empty, leaves has_faixa false. Turning any of those into an invalid-response error would tell the caller to retry a CEP that will never exist.
-rw-r--r--Makefile2
-rw-r--r--README.md49
-rw-r--r--app/main.f9014
-rw-r--r--src/cepis.f9018
-rw-r--r--src/cepis_models.f90144
-rw-r--r--test/test_cepis.f90106
6 files changed, 323 insertions, 10 deletions
diff --git a/Makefile b/Makefile
index e649642..2ad30f7 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION := 0.1.0
+VERSION := 0.2.0
ifeq ($(origin FC),default)
FC := gfortran
diff --git a/README.md b/README.md
index 7d6d6ab..c936ceb 100644
--- a/README.md
+++ b/README.md
@@ -252,6 +252,55 @@ end select
`cepis_status_name(status)` gives a short, stable English name for logging.
+### When the CEP does not exist
+
+A CEP that fails is rarely a random number: it is usually someone who knows the
+city and guessed the digits, and the classic guess is the `-000` that 957
+municipalities never received. So a 404 may carry a `faixa` naming the
+municipality the CEP *would* have belonged to. It is on the address as
+`has_faixa` / `faixa`, exactly like `has_geo` / `geo`:
+
+```fortran
+call cepis_lookup('25845-000', a, status, message)
+
+if (status == CEPIS_ERR_NOT_FOUND .and. a%has_faixa) then
+ print *, a%faixa%localidade ! 'Areal'
+ print *, a%faixa%uf ! 'RJ'
+ print *, a%faixa%estado ! 'Rio de Janeiro'
+ print *, a%faixa%regiao ! 'Sudeste'
+ print *, a%faixa%ibge ! '3300225'
+ print *, a%faixa%formatted_cep_ini() ! '25845-001'
+ print *, a%faixa%formatted_cep_fim() ! '25849-899'
+ print *, a%faixa%ceps ! 118
+end if
+```
+
+> **It is still a 404.** `status` is `CEPIS_ERR_NOT_FOUND` and `a%found` is
+> `.false.`. `faixa` is context for the error, never an address: the CEP does
+> not exist and nothing there can be delivered to.
+
+Two things about `cep_ini` and `cep_fim`:
+
+- They are the **smallest and largest CEP that actually exist** in that
+ municipality, not the bounds the Correios publish. The DNE's own start for
+ Areal is `25845000` — precisely the CEP that just failed, which is why the
+ published bound would be no use here.
+- **The range is sparse, and the pair is not a list.** Areal's 118 CEPs are
+ scattered over the ~4,900 numbers between `25845001` and `25849899`; almost
+ none of the numbers in between exist either. That is what `ceps` is for.
+ Show the range, never enumerate it, and check any single number with a
+ lookup.
+
+`has_faixa` is `.false.` whenever the API omits the block — it **omits it, it
+never sends null** — which happens when the CEP falls in a band no municipality
+owns. It is also `.false.` for a 404 whose body is malformed or unintelligible:
+parsing the band can never turn a clean not-found into
+`CEPIS_ERR_INVALID_RESPONSE`, because that would tell you to retry a CEP that
+will never exist.
+
+Only `lookup` carries it. The batch behind `lookup_many` is CSV and has no
+`faixa` column, so a CEP missing from a batch is just `found = .false.`.
+
`CEPIS_ERR_INVALID_RESPONSE` almost always means something between you and the
API is answering instead of it — a captive Wi-Fi portal, a corporate proxy, an
HTML error page. Useful as a diagnosis: the problem is not in your code.
diff --git a/app/main.f90 b/app/main.f90
index 5e3f988..0029d6e 100644
--- a/app/main.f90
+++ b/app/main.f90
@@ -109,6 +109,7 @@ contains
call handle%lookup(cep, address, code, detail)
if (code /= CEPIS_OK) then
write (error_unit, '(A)') 'cepis: '//cepis_status_name(code)//': '//detail
+ if (address%has_faixa) call show_faixa(address%faixa)
stop 1
end if
@@ -132,6 +133,19 @@ contains
end if
end subroutine show_one
+ !> What the API knows about a CEP that does not exist: the municipality it
+ !> would have fallen under. The lookup still failed, so this goes to stderr
+ !> with the error, and the exit status stays 1.
+ subroutine show_faixa(faixa)
+ type(cepis_faixa), intent(in) :: faixa
+
+ write (error_unit, '(A)') ' faixa '//faixa%localidade//'/'// &
+ faixa%uf//' ibge '//faixa%ibge
+ write (error_unit, '(A,I0,A)') ' existentes '//faixa%formatted_cep_ini()// &
+ '..'//faixa%formatted_cep_fim()//' ', faixa%ceps, &
+ ' CEPs (the range is sparse; most numbers in it do not exist)'
+ end subroutine show_faixa
+
subroutine show_many(handle, values)
type(cepis_client), intent(in) :: handle
character(len=*), intent(in) :: values(:)
diff --git a/src/cepis.f90 b/src/cepis.f90
index 27e2c6d..2c4f687 100644
--- a/src/cepis.f90
+++ b/src/cepis.f90
@@ -43,6 +43,7 @@ module cepis
public :: cepis_status_name
public :: cepis_geo
+ public :: cepis_faixa
public :: cepis_address
public :: cepis_health
public :: cepis_client
@@ -55,7 +56,7 @@ module cepis
public :: cepis_format_cep
!> Library version.
- character(len=*), parameter, public :: CEPIS_VERSION = '0.1.0'
+ character(len=*), parameter, public :: CEPIS_VERSION = '0.2.0'
!> Default API endpoint.
character(len=*), parameter, public :: CEPIS_DEFAULT_BASE_URL = 'https://cep.is'
@@ -286,6 +287,12 @@ contains
!> server knows something this client does not.
!>
!> This is the only call that carries `has_geo` and `geo`.
+ !>
+ !> A 404 is `CEPIS_ERR_NOT_FOUND` and `address%found` stays `.false.`, but
+ !> the API may still say which municipality the CEP *would* have belonged
+ !> to. When it does, `address%has_faixa` is `.true.` and `address%faixa`
+ !> names it — see `cepis_faixa`. It is context for the failure, never an
+ !> address: the call still failed and the CEP still does not exist.
subroutine client_lookup(self, cep, address, status, message, http_status, retry_after)
class(cepis_client), intent(in) :: self
character(len=*), intent(in) :: cep
@@ -315,6 +322,11 @@ contains
if (response%status == 404) then
status = CEPIS_ERR_NOT_FOUND
message = 'CEP '//digits//' does not exist in the DNE database'
+ call address_faixa_from_body(response%body, address)
+ if (address%has_faixa) then
+ message = message//' — its band belongs to '//address%faixa%localidade
+ if (len(address%faixa%uf) > 0) message = message//'/'//address%faixa%uf
+ end if
return
end if
@@ -355,7 +367,9 @@ contains
!> **The batch carries no coordinate.** The endpoint behind it is CSV, and
!> the CSV has no `geo` column. Every address returned here has
!> `has_geo = .false.`, including for CEPs that do have one. Need `geo`?
- !> Use `lookup`, one at a time.
+ !> Use `lookup`, one at a time. The same goes for `has_faixa`: the CSV has
+ !> no `faixa` column either, so a CEP the batch did not resolve is simply
+ !> `found = .false.` with nothing said about where it would have been.
subroutine client_lookup_many(self, ceps, addresses, status, message, &
http_status, retry_after)
class(cepis_client), intent(in) :: self
diff --git a/src/cepis_models.f90 b/src/cepis_models.f90
index c50d206..c58c1ab 100644
--- a/src/cepis_models.f90
+++ b/src/cepis_models.f90
@@ -12,16 +12,18 @@
!> come from the Correios database.
module cepis_models
use, intrinsic :: iso_fortran_env, only: real64, int64
- use cepis_json, only: json_document, JSON_NUMBER, JSON_STRING, JSON_OBJECT
+ use cepis_json, only: json_document, json_parse, JSON_NUMBER, JSON_STRING, JSON_OBJECT
use cepis_strings, only: parse_real, is_digits
implicit none
private
public :: cepis_geo
+ public :: cepis_faixa
public :: cepis_address
public :: cepis_health
public :: address_clear
public :: address_from_json
+ public :: address_faixa_from_body
public :: health_from_json
public :: cepis_format_cep
@@ -57,6 +59,51 @@ module cepis_models
procedure :: is_precise
end type cepis_geo
+ !> The municipality a CEP that does **not** exist would have belonged to.
+ !>
+ !> This is **context for an error, not an address**. It arrives with a 404
+ !> and it leaves the lookup a 404: nothing here says the CEP exists, and
+ !> nothing here may be delivered to. A CEP that fails is rarely a random
+ !> number — it is usually someone who knows the city and guessed the digits,
+ !> and the classic guess is the `-000` that 957 municipalities never
+ !> received. `faixa` is what lets you answer *"25845-000 is not a CEP; did
+ !> you mean one in Areal/RJ?"* instead of a bare "not found".
+ !>
+ !> `cep_ini` and `cep_fim` are the smallest and the largest CEP that
+ !> **actually exist** in that municipality, which is not what the Correios
+ !> publish as the band's bounds. The DNE's own start for Areal is
+ !> `25845000` — precisely the CEP that just failed, which is why the
+ !> published bound would be useless here.
+ !>
+ !> **The range is sparse, and the pair is not a list.** Areal's 118 CEPs are
+ !> scattered over the roughly 4,900 numbers between `25845001` and
+ !> `25849899`; the overwhelming majority of the numbers in between do not
+ !> exist either. `ceps` is there to make that impossible to miss. Never
+ !> enumerate `cep_ini`..`cep_fim` and never offer it as a set of valid CEPs;
+ !> use it to show a range, and validate any single number with a lookup.
+ type :: cepis_faixa
+ !> Municipality name, e.g. `Areal`.
+ character(len=:), allocatable :: localidade
+ !> Two-letter state code, e.g. `RJ`.
+ character(len=:), allocatable :: uf
+ character(len=:), allocatable :: estado
+ character(len=:), allocatable :: regiao
+ !> IBGE municipality code.
+ character(len=:), allocatable :: ibge
+ !> Smallest CEP that exists in the municipality, 8 digits, unmasked.
+ character(len=:), allocatable :: cep_ini
+ !> Largest CEP that exists in the municipality, 8 digits, unmasked.
+ character(len=:), allocatable :: cep_fim
+ !> How many CEPs exist between `cep_ini` and `cep_fim`, inclusive.
+ !> Far fewer than the span between them: the range is sparse.
+ integer :: ceps = 0
+ contains
+ !> `cep_ini` with its mask: `25845-001`. Empty when it is not 8 digits.
+ procedure :: formatted_cep_ini
+ !> `cep_fim` with its mask: `25849-899`. Empty when it is not 8 digits.
+ procedure :: formatted_cep_fim
+ end type cepis_faixa
+
!> A CEP and everything the database knows about it.
!>
!> Every character component is allocated after any successful call. Fields
@@ -64,10 +111,10 @@ module cepis_models
!> unallocated — that is what the API returns, and changing it here would
!> create a difference between reading the JSON and reading this library.
!>
- !> `geo` is the exception, and it is guarded by `has_geo` rather than by an
- !> unallocated component: the API **omits** the whole field when there is
- !> no coordinate (about 43% of CEPs) rather than sending null, and a flag
- !> makes the check impossible to forget.
+ !> `geo` and `faixa` are the exceptions, and they are guarded by `has_geo`
+ !> and `has_faixa` rather than by an unallocated component: the API
+ !> **omits** either field entirely — it never sends null — and a flag makes
+ !> the check impossible to forget.
type :: cepis_address
!> `.false.` for a CEP that the batch did not resolve. Always `.true.`
!> for an address that came back from a single lookup.
@@ -92,6 +139,14 @@ module cepis_models
logical :: has_geo = .false.
!> The coordinate. Meaningful only when `has_geo` is `.true.`.
type(cepis_geo) :: geo
+ !> `.true.` when `faixa` names the municipality this CEP would have
+ !> belonged to. Only ever set on a `CEPIS_ERR_NOT_FOUND`, and never on
+ !> a CEP that was found — it is the API's context for the 404.
+ logical :: has_faixa = .false.
+ !> The municipality the failing CEP falls under. Meaningful only when
+ !> `has_faixa` is `.true.`, and **not** an address: `found` is still
+ !> `.false.` and the lookup still failed.
+ type(cepis_faixa) :: faixa
!> The response body exactly as it arrived, uninterpreted, so a field
!> the API grows before this library is updated is not lost. Empty for
!> addresses that came from a batch, whose transport is CSV.
@@ -133,6 +188,24 @@ contains
if (allocated(self%precisao)) yes = self%precisao == 'logradouro'
end function is_precise
+ pure function formatted_cep_ini(self) result(text)
+ class(cepis_faixa), intent(in) :: self
+ character(len=:), allocatable :: text
+
+ text = ''
+ if (.not. allocated(self%cep_ini)) return
+ text = cepis_format_cep(self%cep_ini)
+ end function formatted_cep_ini
+
+ pure function formatted_cep_fim(self) result(text)
+ class(cepis_faixa), intent(in) :: self
+ character(len=:), allocatable :: text
+
+ text = ''
+ if (.not. allocated(self%cep_fim)) return
+ text = cepis_format_cep(self%cep_fim)
+ end function formatted_cep_fim
+
pure function health_ok(self) result(yes)
class(cepis_health), intent(in) :: self
logical :: yes
@@ -188,6 +261,7 @@ contains
address%ddd = ''
address%siafi = ''
address%has_geo = .false.
+ address%has_faixa = .false.
address%raw = ''
end subroutine address_clear
@@ -252,6 +326,66 @@ contains
address%has_geo = .true.
end subroutine read_geo
+ !> Read the `faixa` block out of the body of a 404, if it has one.
+ !>
+ !> Takes the raw body rather than a parsed document because the caller has
+ !> not parsed it: a not-found is decided by the HTTP status, and the body
+ !> of an error is never required to be intelligible.
+ !>
+ !> **Every failure here is silent** and leaves `has_faixa = .false.` — a
+ !> body that is not JSON, a `faixa` that is absent, null, not an object, or
+ !> empty. Turning any of those into a parse error would replace a clean
+ !> `CEPIS_ERR_NOT_FOUND` with `CEPIS_ERR_INVALID_RESPONSE` and tell the
+ !> caller to retry a CEP that will never exist. The 404 is the answer; the
+ !> `faixa` is a bonus, and a bonus may not cost the answer.
+ subroutine address_faixa_from_body(body, address)
+ character(len=*), intent(in) :: body
+ type(cepis_address), intent(inout) :: address
+ type(json_document) :: doc
+ character(len=:), allocatable :: detail
+ logical :: ok
+
+ address%has_faixa = .false.
+ call json_parse(body, doc, ok, detail)
+ if (.not. ok) return
+ if (doc%kind_of(doc%root) /= JSON_OBJECT) return
+
+ call read_faixa(doc, doc%member(doc%root, 'faixa'), address)
+ end subroutine address_faixa_from_body
+
+ subroutine read_faixa(doc, node, address)
+ type(json_document), intent(in) :: doc
+ integer, intent(in) :: node
+ type(cepis_address), intent(inout) :: address
+ character(len=:), allocatable :: localidade
+ real(real64) :: count
+ logical :: ok
+
+ address%has_faixa = .false.
+ if (doc%kind_of(node) /= JSON_OBJECT) return
+
+ ! The municipality name is the whole point of the block: without it
+ ! there is nothing to tell the caller, so an empty one is no faixa.
+ localidade = doc%field_text(node, 'localidade')
+ if (len(localidade) == 0) return
+
+ address%faixa%localidade = localidade
+ address%faixa%uf = doc%field_text(node, 'uf')
+ address%faixa%estado = doc%field_text(node, 'estado')
+ address%faixa%regiao = doc%field_text(node, 'regiao')
+ address%faixa%ibge = doc%field_text(node, 'ibge')
+ address%faixa%cep_ini = doc%field_text(node, 'cep_ini')
+ address%faixa%cep_fim = doc%field_text(node, 'cep_fim')
+
+ ! A missing count leaves zero rather than dropping the block: the name
+ ! of the municipality is still worth having.
+ address%faixa%ceps = 0
+ call number_field(doc, node, 'ceps', count, ok)
+ if (ok) address%faixa%ceps = int(count)
+
+ address%has_faixa = .true.
+ end subroutine read_faixa
+
subroutine number_field(doc, node, name, value, ok)
type(json_document), intent(in) :: doc
integer, intent(in) :: node
diff --git a/test/test_cepis.f90 b/test/test_cepis.f90
index 348e9df..9f22deb 100644
--- a/test/test_cepis.f90
+++ b/test/test_cepis.f90
@@ -16,7 +16,7 @@ program test_cepis
use test_server
implicit none
- character(len=:), allocatable :: SE, PAULISTA, CITYWIDE, CSV_HEADER, HEALTH_BODY
+ character(len=:), allocatable :: SE, PAULISTA, CITYWIDE, AREAL, CSV_HEADER, HEALTH_BODY
integer :: passed, failed
type(c_ptr) :: server
character(len=:), allocatable :: base
@@ -48,6 +48,8 @@ program test_cepis
call test_malformed_geo_does_not_lose_the_address()
call test_new_api_field_does_not_break()
call test_not_found_is_not_retried()
+ call test_not_found_carries_the_band()
+ call test_faixa_never_costs_the_not_found()
call test_invalid_cep_never_leaves_the_machine()
call test_api_400_becomes_invalid_cep()
call test_response_that_is_not_json()
@@ -74,7 +76,7 @@ program test_cepis
call test_status_names_exist()
call server_stop(server)
- deallocate (SE, PAULISTA, CITYWIDE, CSV_HEADER, HEALTH_BODY, base)
+ deallocate (SE, PAULISTA, CITYWIDE, AREAL, CSV_HEADER, HEALTH_BODY, base)
write (output_unit, '(A)') ''
write (output_unit, '(A,I0,A,I0,A)') 'passed ', passed, ', failed ', failed, ''
@@ -102,6 +104,17 @@ contains
CITYWIDE = CITYWIDE//'"gia":"","ddd":"68","siafi":"0003","geo":{"lat":-9.9754,'
CITYWIDE = CITYWIDE//'"lon":-67.8249,"raio_m":21400,"precisao":"localidade"}}'
+ ! A 404 that carries the band. 25845000 is the DNE's own published
+ ! start for Areal and does not exist; the CEPs that do start at
+ ! 25845001, and the 118 of them are scattered over the ~4,900 numbers
+ ! up to 25849899.
+ AREAL = '{"erro":"cep_nao_encontrado",'
+ AREAL = AREAL//'"mensagem":"CEP n\u00e3o existe na base do DNE",'
+ AREAL = AREAL//'"faixa":{"localidade":"Areal","uf":"RJ",'
+ AREAL = AREAL//'"estado":"Rio de Janeiro","regiao":"Sudeste",'
+ AREAL = AREAL//'"ibge":"3300225","cep_ini":"25845001",'
+ AREAL = AREAL//'"cep_fim":"25849899","ceps":118}}'
+
CSV_HEADER = 'cep,logradouro,complemento,unidade,bairro,localidade,uf,'
CSV_HEADER = CSV_HEADER//'estado,regiao,ibge,gia,ddd,siafi'
@@ -457,8 +470,97 @@ contains
call check(status == CEPIS_ERR_NOT_FOUND, '404 becomes not found')
call check(server_count(server, '/99999999') == 1, '404 is not retried')
call check(len(message) > 0, 'not found explains itself')
+ call check(.not. address%has_faixa, 'a 404 with no faixa leaves has_faixa false')
end subroutine test_not_found_is_not_retried
+ !> A 404 that names the municipality the CEP would have belonged to.
+ !>
+ !> 25845-000 is the case the field exists for: the DNE's own published
+ !> start for Areal, and a number that does not exist. Someone who knows the
+ !> city guessed the `-000`, as they do in the 957 municipalities that never
+ !> received one.
+ subroutine test_not_found_carries_the_band()
+ type(cepis_client) :: client
+ type(cepis_address) :: address
+ character(len=:), allocatable :: message
+ integer :: status
+
+ call server_reset(server)
+ call server_respond(server, '/25845000', 404, AREAL, '')
+ call local_client(client)
+
+ call client%lookup('25845000', address, status, message)
+ call check(status == CEPIS_ERR_NOT_FOUND, 'a faixa does not make the 404 a success')
+ call check(.not. address%found, 'the address is still not found')
+ call check(server_count(server, '/25845000') == 1, 'a 404 with a faixa is not retried')
+
+ call check(address%has_faixa, 'has_faixa is true when the block is present')
+ call check(address%faixa%localidade == 'Areal', 'faixa names the municipality')
+ call check(address%faixa%uf == 'RJ', 'faixa uf')
+ call check(address%faixa%estado == 'Rio de Janeiro', 'faixa estado')
+ call check(address%faixa%regiao == 'Sudeste', 'faixa regiao')
+ call check(address%faixa%ibge == '3300225', 'faixa ibge')
+ call check(index(message, 'Areal') > 0, 'the message names the municipality')
+
+ ! The bounds are the CEPs that exist, not the band the Correios
+ ! publish: the published start is 25845000, the CEP that just failed.
+ call check(address%faixa%cep_ini == '25845001', 'cep_ini is the smallest that exists')
+ call check(address%faixa%cep_fim == '25849899', 'cep_fim is the largest that exists')
+ call check(address%faixa%formatted_cep_ini() == '25845-001', 'cep_ini masks')
+ call check(address%faixa%formatted_cep_fim() == '25849-899', 'cep_fim masks')
+
+ ! And they are not a list: 118 CEPs over ~4,900 numbers.
+ call check(address%faixa%ceps == 118, 'ceps counts what actually exists')
+ call check(address%faixa%ceps < 4899, 'the range is sparser than its span')
+ end subroutine test_not_found_carries_the_band
+
+ !> Nothing about the faixa may turn a clean not-found into a failure.
+ subroutine test_faixa_never_costs_the_not_found()
+ type(cepis_client) :: client
+ type(cepis_address) :: address
+ character(len=:), allocatable :: message
+ integer :: status
+
+ call server_reset(server)
+ call local_client(client)
+
+ call server_respond(server, '/99999991', 404, &
+ '{"erro":"cep_nao_encontrado","faixa":null}', '')
+ call client%lookup('99999991', address, status, message)
+ call check(status == CEPIS_ERR_NOT_FOUND, 'a null faixa is still a not found')
+ call check(.not. address%has_faixa, 'a null faixa is absent, not present and empty')
+
+ call server_respond(server, '/99999992', 404, &
+ '{"erro":"cep_nao_encontrado","faixa":"Areal"}', '')
+ call client%lookup('99999992', address, status, message)
+ call check(status == CEPIS_ERR_NOT_FOUND, 'a faixa that is not an object still 404s')
+ call check(.not. address%has_faixa, 'a faixa that is not an object is dropped')
+
+ call server_respond(server, '/99999993', 404, &
+ '{"erro":"cep_nao_encontrado","faixa":{}}', '')
+ call client%lookup('99999993', address, status, message)
+ call check(status == CEPIS_ERR_NOT_FOUND, 'an empty faixa still 404s')
+ call check(.not. address%has_faixa, 'a faixa with no municipality is no faixa')
+
+ call server_respond(server, '/99999994', 404, '<html>not found</html>', '')
+ call client%lookup('99999994', address, status, message)
+ call check(status == CEPIS_ERR_NOT_FOUND, 'a 404 body that is not JSON still 404s')
+ call check(.not. address%has_faixa, 'an unparseable 404 body carries no faixa')
+
+ call server_respond(server, '/99999995', 404, '', '')
+ call client%lookup('99999995', address, status, message)
+ call check(status == CEPIS_ERR_NOT_FOUND, 'an empty 404 body still 404s')
+ call check(.not. address%has_faixa, 'an empty 404 body carries no faixa')
+
+ call server_respond(server, '/99999996', 404, &
+ '{"erro":"cep_nao_encontrado","faixa":{"localidade":"Areal",'// &
+ '"uf":"RJ","cep_ini":"25845001","cep_fim":"25849899"}}', '')
+ call client%lookup('99999996', address, status, message)
+ call check(address%has_faixa, 'a faixa missing its count is still worth having')
+ call check(address%faixa%ceps == 0, 'a missing count is zero, not a dropped block')
+ call check(address%faixa%estado == '', 'a missing string field is empty')
+ end subroutine test_faixa_never_costs_the_not_found
+
subroutine test_invalid_cep_never_leaves_the_machine()
type(cepis_client) :: client
type(cepis_address) :: address