From c3b525537e944064869a2cd6495733388144ce64 Mon Sep 17 00:00:00 2001 From: Murilo Ijanc Date: Mon, 27 Jul 2026 20:42:22 -0300 Subject: Surface the faixa a 404 now carries A CEP that fails is almost never a random number. It is someone who knows the city and guessed the last digits, and the guess is usually the -000 that 957 municipalities never received. The API answers those 404s with a faixa object naming the municipality that owns the band, which turns "not found" from a dead end into something the caller can act on - and the caller cannot reach it through this library, because the 404 path never looked at the body at all. cepis_error is where a 404 already reports itself, so faixa goes there, as inline storage rather than a pointer: the struct is documented as owning nothing and declared on the caller's stack, and that stays true. It costs sizeof(cepis_error), 272 bytes to 384, so 0.2.0 needs a recompile. The parser needed one new primitive. cepis__json_field could only pull scalars out of a flat object; splitting it over cepis__json_find, which returns a pointer to a value of any shape, reaches one level down without building a document tree - the same depth cepis__geo_parse already needs for geo. Absence is the case that matters: no faixa, an empty one, or one that is not an object all leave the error byte-for-byte what it was before. cep_ini and cep_fim are deliberately not presented as a range. They are the lowest and highest CEP that exist, the band is sparse - Areal's 118 CEPs are scattered over ~4900 numbers - and ceps is the only honest count. --- include/cepis.h | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 6 deletions(-) (limited to 'include') diff --git a/include/cepis.h b/include/cepis.h index ea8d378..a00982a 100644 --- a/include/cepis.h +++ b/include/cepis.h @@ -18,7 +18,7 @@ extern "C" { #endif -#define CEPIS_VERSION "0.1.0" +#define CEPIS_VERSION "0.2.0" /** Largest number of CEPs the API accepts in one /csv request. */ #define CEPIS_MAX_BATCH 500 @@ -26,6 +26,12 @@ extern "C" { /** Size of cepis_error::message, including the terminating NUL. */ #define CEPIS_MESSAGE_MAX 256 +/** Size of cepis_faixa::localidade, including the terminating NUL. The + * longest municipality name in the IBGE list is 32 characters; this leaves + * room for it in UTF-8 twice over. A longer name is truncated, never + * overflowed. */ +#define CEPIS_FAIXA_LOCALIDADE_MAX 72 + /** * Outcome of a call. Split by the decision the caller has to make, not by the * HTTP status that caused it: CEPIS_ERR_INVALID_CEP and CEPIS_ERR_NOT_FOUND @@ -144,11 +150,55 @@ struct cepis_health { char *raw; }; +/** + * The municipality a CEP that does not exist would have belonged to. + * + * 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. So the API answers a 404 with the + * band the number falls in, when a municipality owns that band. This is + * context for an error and **not an address**: the lookup still failed, and + * cepis_lookup() still returns CEPIS_ERR_NOT_FOUND. + * + * Everything here is inline storage inside cepis_error - nothing to free, + * nothing that can be NULL. Read it only when `present` is non-zero; when it + * is zero the strings are `""` and `ceps` is 0. + * + * `cep_ini` and `cep_fim` are the **smallest and largest CEP that actually + * exist** in that municipality, not the band the Correios publish. For Areal + * the DNE's own band starts at 25845000 - exactly the CEP that just failed - + * while `cep_ini` is 25845001. + * + * The band is sparse. Areal's 118 CEPs are scattered over the ~4900 numbers + * between 25845001 and 25849899, so `ceps` is a count and the pair is a pair + * of endpoints. Never present the two as a range of valid CEPs, and never + * iterate it. + */ +typedef struct cepis_faixa { + /** Non-zero when the API named a municipality for this CEP. Zero means it + * did not - the field is omitted, never null, for a number in a band no + * municipality owns. */ + int present; + /** Municipality name, e.g. "Areal". Truncated at + * CEPIS_FAIXA_LOCALIDADE_MAX - 1 bytes. */ + char localidade[CEPIS_FAIXA_LOCALIDADE_MAX]; + /** Two-letter state, e.g. "RJ". */ + char uf[3]; + /** Smallest CEP that exists there, 8 digits unmasked. */ + char cep_ini[9]; + /** Largest CEP that exists there, 8 digits unmasked. */ + char cep_fim[9]; + /** How many CEPs exist there. Far smaller than cep_fim - cep_ini. */ + long long ceps; +} cepis_faixa; + /** * Detail about a failure. Optional: pass NULL to any call that takes one. * * Caller-allocated, no ownership: declare one on the stack and reuse it. It is - * fully overwritten at the start of every call that receives it. + * fully overwritten at the start of every call that receives it, `faixa` + * included, so a band left over from a previous 404 cannot be mistaken for + * this one's. */ typedef struct cepis_error { /** Same value the call returned. */ @@ -159,8 +209,12 @@ typedef struct cepis_error { /** Seconds the server asked for via Retry-After, or 0 if it did not say. * Only meaningful for CEPIS_ERR_RATE_LIMITED. */ double retry_after; - /** Human-readable description, NUL-terminated, empty on success. */ + /** Human-readable description, NUL-terminated, empty on success. On a 404 + * with a `faixa` it names the municipality too. */ char message[CEPIS_MESSAGE_MAX]; + /** Only ever filled by CEPIS_ERR_NOT_FOUND, and only when the API sent a + * band. See cepis_faixa. */ + cepis_faixa faixa; } cepis_error; /** @@ -192,7 +246,7 @@ typedef struct cepis_config { * 0 leaves max_attempts as the only bound. */ double total_timeout; /** Overrides the User-Agent. Default - * "cepis-asm/0.1.0 (+https://cep.is)". */ + * "cepis-asm/0.2.0 (+https://cep.is)". */ const char *user_agent; } cepis_config; @@ -201,7 +255,7 @@ typedef struct cepis_config { * between requests, so one instance is safe to share across threads. */ typedef struct cepis_client cepis_client; -/** Library version, "0.1.0". Static storage; do not free. */ +/** Library version, "0.2.0". Static storage; do not free. */ const char *cepis_version(void); /** One-line description of a cepis_status. Static storage; do not free. */ @@ -263,7 +317,9 @@ cepis_status cepis_normalize_cep(const char *value, char *out); * The CEP is validated locally first: a 7-digit CEP returns * CEPIS_ERR_INVALID_CEP without a request being made. A 400 from the API is * also CEPIS_ERR_INVALID_CEP, carrying the API's own message. A 404 is - * CEPIS_ERR_NOT_FOUND. Neither is retried; 429, 5xx and network failures are. + * CEPIS_ERR_NOT_FOUND, and fills cepis_error::faixa when the API says which + * municipality owns that band. Neither is retried; 429, 5xx and network + * failures are. */ cepis_status cepis_lookup(cepis_client *client, const char *cep, cepis_address **out, cepis_error *err); -- cgit v1.3