summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-27 20:39:06 -0300
committerMurilo Ijanc <[email protected]>2026-07-27 20:39:06 -0300
commit497b1c1b01f6c9a584e5b40e02aff4763447529c (patch)
treeb50feb4252ba93cf045be3aaac14a96eeb04dff5 /src
parent87166a5299aa551517a86379a0c05eb2110b52b0 (diff)
downloadzig-497b1c1b01f6c9a584e5b40e02aff4763447529c.tar.gz
Report the municipality behind a CEP that does not existHEADv0.2.0main
A CEP that fails to resolve is rarely a random number. It is almost always someone who knows the city and guessed the digits, and the classic guess is the -000 that 957 municipalities never received. The API now says so: a 404 carries an optional faixa block naming the municipality the number would have belonged to, and answering "no such CEP in Areal/RJ, which has 118 of them" is a great deal more useful than "not found". It arrives through Diagnostics, which is where every other detail a Zig error tag cannot carry already travels, and it is read with diag.faixa(). Like message(), it lives in a fixed buffer inside the value and allocates nothing, so no caller has to learn a new deinit for it. The lookup still fails. faixa is context for the error, never an address, and cep_ini/cep_fim are the smallest and largest CEP that actually exist there rather than the bounds the Correios publish — for Areal the DNE's own start is 25845000, which is exactly the CEP that failed. The range is also sparse, 118 CEPs over ~4,900 numbers, so ceps is the count and the pair must never be presented as a list. The doc comments and the README say all three, because this field is easy to misread as a success. Parsing is deliberately inert: a faixa that is missing, null, not an object, nameless or oversized yields null, and an unreadable body is skipped. Nothing about this field may promote a clean not-found into a parse failure.
Diffstat (limited to 'src')
-rw-r--r--src/client.zig69
-rw-r--r--src/errors.zig128
-rw-r--r--src/main.zig7
-rw-r--r--src/model.zig196
-rw-r--r--src/root.zig5
5 files changed, 400 insertions, 5 deletions
diff --git a/src/client.zig b/src/client.zig
index 3a726e5..2c71e16 100644
--- a/src/client.zig
+++ b/src/client.zig
@@ -22,7 +22,7 @@ const Health = model.Health;
const note = errors.note;
/// The version of this library, as sent in the `User-Agent`.
-pub const version = "0.1.0";
+pub const version = "0.2.0";
/// Where the public API lives.
pub const default_url = "https://cep.is";
@@ -238,6 +238,11 @@ pub const Client = struct {
/// in `Diagnostics`: the format already passed local validation, so the
/// server knows something this client does not.
///
+ /// A 404 becomes `error.NotFound`, and the 404 body may name the
+ /// municipality the CEP would have belonged to — read it with
+ /// `Diagnostics.faixa`. It stays an error: the CEP does not exist, and the
+ /// range is context for saying so, not an address.
+ ///
/// Errors: `error.InvalidCep` (not 8 digits — no request is made),
/// `error.NotFound`, `error.RateLimited`, `error.ServerError`,
/// `error.NetworkError`, `error.InvalidResponse`, `error.OutOfMemory`.
@@ -259,6 +264,7 @@ pub const Client = struct {
defer c.gpa.free(response.body);
if (response.status == 404) {
+ noteFaixa(options.diagnostics, response.body);
note(options.diagnostics, "CEP {s} does not exist in the DNE database", .{number});
return error.NotFound;
}
@@ -746,6 +752,35 @@ fn noteApiMessage(
d.setMessage(fallback, args);
}
+/// Record the `faixa` a 404 body may carry — the municipality the CEP would
+/// have belonged to — into `diagnostics`.
+///
+/// Every failure here is silent and leaves `Diagnostics.faixa` null. The answer
+/// is already `error.NotFound`, and no body, however mangled, may promote a
+/// clean not-found into `error.InvalidResponse`. The parse runs against a fixed
+/// stack buffer, so a body too large for it is simply skipped: nothing is
+/// allocated from `gpa` on this path and nothing can leak.
+///
+/// The *leaky* parse is the point of the fixed buffer: the arena
+/// `parseFromSlice` builds asks its backing allocator for a node far larger
+/// than the document, which a stack buffer this size cannot serve. Here the
+/// buffer *is* the arena, and it is discarded whole on return.
+fn noteFaixa(diagnostics: ?*Diagnostics, body: []const u8) void {
+ const d = diagnostics orelse return;
+ d.clearFaixa();
+
+ var buffer: [4096]u8 = undefined;
+ var fba: std.heap.FixedBufferAllocator = .init(&buffer);
+ const value = std.json.parseFromSliceLeaky(
+ std.json.Value,
+ fba.allocator(),
+ body,
+ .{},
+ ) catch return;
+
+ d.setFaixa(value);
+}
+
fn preview(body: []const u8) []const u8 {
return body[0..@min(body.len, 120)];
}
@@ -793,6 +828,38 @@ test "only transient statuses are retriable" {
}
}
+test "noteFaixa reads the range a 404 carries" {
+ var diag: Diagnostics = .{};
+ noteFaixa(&diag,
+ \\{"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE",
+ \\ "faixa":{"localidade":"Areal","uf":"RJ","ibge":"3300225",
+ \\ "cep_ini":"25845001","cep_fim":"25849899","ceps":118}}
+ );
+ const faixa = diag.faixa().?;
+ try testing.expectEqualStrings("Areal", faixa.localidade);
+ try testing.expectEqual(@as(u64, 118), faixa.ceps);
+}
+
+test "noteFaixa reports nothing rather than failing" {
+ var diag: Diagnostics = .{};
+ for ([_][]const u8{
+ "<html>",
+ "",
+ \\{"erro":"cep_nao_encontrado"}
+ ,
+ \\{"faixa":[]}
+ ,
+ \\{"faixa":{"uf":"RJ"}}
+ ,
+ }) |body| {
+ noteFaixa(&diag, body);
+ try testing.expect(diag.faixa() == null);
+ }
+
+ // Null works the same way with no `Diagnostics` at all.
+ noteFaixa(null, "{}");
+}
+
test "noteApiMessage prefers the API wording" {
var diag: Diagnostics = .{};
noteApiMessage(&diag, "{\"erro\":\"cep_invalido\",\"mensagem\":\"rejected\"}", "fallback {s}", .{"x"});
diff --git a/src/errors.zig b/src/errors.zig
index c46e266..5ce474a 100644
--- a/src/errors.zig
+++ b/src/errors.zig
@@ -18,10 +18,13 @@
//!
//! A Zig error carries no payload, so anything a caller may need beyond the
//! tag — the HTTP status, the `Retry-After` the server asked for, the API's own
-//! message — travels in an optional `Diagnostics` the caller passes in.
+//! message, the municipality a missing CEP would have belonged to — travels in
+//! an optional `Diagnostics` the caller passes in.
const std = @import("std");
+const Faixa = @import("model.zig").Faixa;
+
/// Everything that can go wrong when talking to the API, minus allocation
/// failure. Use `Error` in signatures; use this when you want to match
/// exhaustively without `error.OutOfMemory` in the way.
@@ -35,6 +38,10 @@ pub const CepisError = error{
InvalidCep,
/// The CEP is well-formed but does not exist in the DNE database.
+ ///
+ /// `Diagnostics.faixa` may name the municipality the CEP would have
+ /// belonged to. That is context for the failure, not an address — the
+ /// lookup found nothing either way.
NotFound,
/// The API (or the edge in front of it) answered 429 on every attempt.
@@ -91,9 +98,9 @@ pub fn isRetriable(err: Error) bool {
/// };
/// ```
///
-/// It owns no memory and needs no `deinit`: the message lives in a fixed
-/// buffer inside the struct, and is only valid as long as the `Diagnostics`
-/// itself. Copy it if you need it to outlive the value.
+/// It owns no memory and needs no `deinit`: the message and the range live in
+/// fixed buffers inside the struct, and are only valid as long as the
+/// `Diagnostics` itself. Copy them if you need them to outlive the value.
///
/// The fields describe the last response the call saw, so `status` is also
/// filled after a call that succeeded. They are only *meaningful* after a
@@ -117,6 +124,17 @@ pub const Diagnostics = struct {
/// Length of the text in `message_buffer`. Internal.
message_len: usize = 0,
+ /// Backing storage for `faixa`. Internal; read it through `faixa`.
+ faixa_buffer: [Faixa.max_bytes]u8 = undefined,
+
+ /// Length of each of `faixa`'s strings inside `faixa_buffer`, in
+ /// `Faixa.text_fields` order, or null when no range was reported.
+ /// Internal.
+ faixa_lengths: ?[Faixa.text_fields.len]u16 = null,
+
+ /// The `ceps` count of the reported range. Internal. Read through `faixa`.
+ faixa_ceps: u64 = 0,
+
/// The longest explanation a `Diagnostics` will keep. Longer text is
/// truncated rather than allocated.
pub const max_message_len = 512;
@@ -127,12 +145,47 @@ pub const Diagnostics = struct {
return d.message_buffer[0..d.message_len];
}
+ /// After `error.NotFound`, the municipality the CEP would have belonged to.
+ ///
+ /// **The lookup still failed.** A non-null `Faixa` explains *where* the
+ /// number would have fallen; it is not the address, and there is no address
+ /// to be had. Use it to answer better — "no such CEP in Areal/RJ, which has
+ /// 118 of them" — not to carry on as though the CEP existed.
+ ///
+ /// Null whenever the API said nothing: the CEP fell in a band no
+ /// municipality owns (the API **omits** the field there, it never sends
+ /// null), the response predates the field, or the body could not be read.
+ /// Null is therefore ordinary, not a sign of trouble.
+ ///
+ /// Like `message`, the value borrows this `Diagnostics` and is valid only
+ /// as long as it is.
+ ///
+ /// ```zig
+ /// client.lookup(cep, .{ .diagnostics = &diag }) catch |err| switch (err) {
+ /// error.NotFound => if (diag.faixa()) |f| {
+ /// std.log.info("no such CEP in {s}/{s} ({d} CEPs)", .{ f.localidade, f.uf, f.ceps });
+ /// },
+ /// else => return err,
+ /// };
+ /// ```
+ pub fn faixa(d: *const Diagnostics) ?Faixa {
+ const lengths = d.faixa_lengths orelse return null;
+ var found: Faixa = .{ .ceps = d.faixa_ceps };
+ var cursor: usize = 0;
+ inline for (Faixa.text_fields, 0..) |name, i| {
+ @field(found, name) = d.faixa_buffer[cursor..][0..lengths[i]];
+ cursor += lengths[i];
+ }
+ return found;
+ }
+
/// Clear every field, so the value can be reused for another call.
pub fn reset(d: *Diagnostics) void {
d.status = null;
d.retry_after_ms = null;
d.attempts = 0;
d.message_len = 0;
+ d.clearFaixa();
}
/// Set the message, truncating at `max_message_len`. Internal.
@@ -141,6 +194,26 @@ pub const Diagnostics = struct {
writer.print(fmt, args) catch {};
d.message_len = writer.end;
}
+
+ /// Record the `faixa` block of a 404 body, or clear it when the body has
+ /// none this can use. Never fails. Internal.
+ pub fn setFaixa(d: *Diagnostics, value: std.json.Value) void {
+ d.clearFaixa();
+ const found = Faixa.fromJson(value, &d.faixa_buffer) orelse return;
+
+ var lengths: [Faixa.text_fields.len]u16 = undefined;
+ inline for (Faixa.text_fields, 0..) |name, i| {
+ lengths[i] = @intCast(@field(found, name).len);
+ }
+ d.faixa_lengths = lengths;
+ d.faixa_ceps = found.ceps;
+ }
+
+ /// Forget any range previously recorded. Internal.
+ pub fn clearFaixa(d: *Diagnostics) void {
+ d.faixa_lengths = null;
+ d.faixa_ceps = 0;
+ }
};
/// Write to `maybe_diagnostics` when the caller supplied one. Internal.
@@ -174,3 +247,50 @@ test "Diagnostics truncates instead of overflowing" {
diag.setMessage("{s}", .{"x" ** (Diagnostics.max_message_len * 2)});
try std.testing.expectEqual(Diagnostics.max_message_len, diag.message().len);
}
+
+test "Diagnostics keeps a faixa without allocating" {
+ const body =
+ \\{"erro":"cep_nao_encontrado","faixa":{"localidade":"Areal","uf":"RJ",
+ \\ "estado":"Rio de Janeiro","regiao":"Sudeste","ibge":"3300225",
+ \\ "cep_ini":"25845001","cep_fim":"25849899","ceps":118}}
+ ;
+ const parsed = try std.json.parseFromSlice(std.json.Value, std.testing.allocator, body, .{});
+ defer parsed.deinit();
+
+ var diag: Diagnostics = .{};
+ try std.testing.expect(diag.faixa() == null);
+
+ diag.setFaixa(parsed.value);
+ const found = diag.faixa().?;
+ try std.testing.expectEqualStrings("Areal", found.localidade);
+ try std.testing.expectEqualStrings("RJ", found.uf);
+ try std.testing.expectEqualStrings("Rio de Janeiro", found.estado);
+ try std.testing.expectEqualStrings("25845001", found.cep_ini);
+ try std.testing.expectEqualStrings("25849899", found.cep_fim);
+ try std.testing.expectEqual(@as(u64, 118), found.ceps);
+
+ diag.reset();
+ try std.testing.expect(diag.faixa() == null);
+}
+
+test "a 404 without a faixa clears the one before it" {
+ const with =
+ \\{"faixa":{"localidade":"Areal","uf":"RJ","ceps":118}}
+ ;
+ const without =
+ \\{"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE"}
+ ;
+
+ var diag: Diagnostics = .{};
+ inline for (.{ with, without }) |body| {
+ const parsed = try std.json.parseFromSlice(
+ std.json.Value,
+ std.testing.allocator,
+ body,
+ .{},
+ );
+ defer parsed.deinit();
+ diag.setFaixa(parsed.value);
+ }
+ try std.testing.expect(diag.faixa() == null);
+}
diff --git a/src/main.zig b/src/main.zig
index 6bb9777..77baae8 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -79,6 +79,13 @@ pub fn main(init: std.process.Init) !u8 {
if (ceps.items.len == 1) {
var address = client.lookup(ceps.items[0], .{ .diagnostics = &diag }) catch |err| {
try out.print("{s}: {t}: {s}\n", .{ ceps.items[0], err, diag.message() });
+ // Not an address — the lookup failed. Only where the number would
+ // have landed, which is usually the useful thing to say back.
+ if (diag.faixa()) |faixa| {
+ try out.print(" would be in {s}/{s}, which has {d} CEPs between {s} and {s}\n", .{
+ faixa.localidade, faixa.uf, faixa.ceps, faixa.cep_ini, faixa.cep_fim,
+ });
+ }
return 1;
};
defer address.deinit(gpa);
diff --git a/src/model.zig b/src/model.zig
index 10ecbd3..8fa1673 100644
--- a/src/model.zig
+++ b/src/model.zig
@@ -16,6 +16,9 @@
//! fields points into memory the value allocated, and `deinit` releases it.
//! Pass the same allocator the `Client` was built with, and treat every slice
//! as invalid afterwards.
+//!
+//! `Faixa` is the exception: it allocates nothing and owns nothing. It travels
+//! inside a `Diagnostics`, which is a stack value, and borrows its storage.
const std = @import("std");
const Allocator = std.mem.Allocator;
@@ -264,6 +267,119 @@ pub const Address = struct {
}
};
+/// The municipality a CEP that does **not** exist would have belonged to.
+///
+/// `GET /{cep}` still answers **404** for a CEP the DNE does not have. When the
+/// number falls inside a band a municipality owns, that 404 now carries this
+/// block as well. It is **context for an error, never an address**: the CEP
+/// asked for does not exist, and nothing here says otherwise. Treating a
+/// non-null `faixa` as a successful lookup turns a clean failure into wrong
+/// data.
+///
+/// It is worth reporting 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 `-000` that 957 municipalities never received.
+///
+/// ## `cep_ini` and `cep_fim` are not the Correios' bounds
+///
+/// They are the smallest and the largest CEP that **actually exist** in that
+/// municipality, not the range the Correios publish. For Areal (RJ) the DNE's
+/// own start is `25845000` — precisely the CEP that just failed — while
+/// `cep_ini` is `25845001`.
+///
+/// ## The range is sparse
+///
+/// Areal's 118 CEPs are scattered across the ~4,900 numbers between `25845001`
+/// and `25849899`. Never present the pair as a list of valid CEPs, and never
+/// count `cep_fim - cep_ini`: `ceps` is the count, and it is here so that
+/// mistake cannot be made by accident.
+///
+/// ## Ownership
+///
+/// Owns nothing and has no `deinit`. Its strings borrow the buffer they were
+/// packed into — the `Diagnostics` that read them — and are valid for exactly
+/// as long as that value is. Copy them if you need them to outlive it.
+pub const Faixa = struct {
+ /// Municipality: `"Areal"`.
+ localidade: []const u8 = "",
+ /// Federative unit, two letters: `"RJ"`.
+ uf: []const u8 = "",
+ /// Federative unit spelled out: `"Rio de Janeiro"`.
+ estado: []const u8 = "",
+ /// Region: `"Sudeste"`.
+ regiao: []const u8 = "",
+ /// IBGE municipality code — the key to join with public datasets.
+ ibge: []const u8 = "",
+ /// The **smallest CEP that exists** in the municipality, not the start of
+ /// the band the Correios published for it.
+ cep_ini: []const u8 = "",
+ /// The **largest CEP that exists** in the municipality.
+ cep_fim: []const u8 = "",
+ /// How many CEPs the municipality actually has between `cep_ini` and
+ /// `cep_fim`. Almost always far fewer than the numbers those two span.
+ ceps: u64 = 0,
+
+ /// The string fields, in the order they are packed into the buffer. Used to
+ /// keep the field list and the JSON keys in step; `localidade` must stay
+ /// first, since it is the one that decides the block says anything at all.
+ pub const text_fields = [_][]const u8{
+ "localidade", "uf", "estado", "regiao",
+ "ibge", "cep_ini", "cep_fim",
+ };
+
+ /// Bytes of storage a `Faixa` can need. The longest municipality name the
+ /// DNE holds is 72 bytes and every other field is short, so this has room
+ /// to spare — see `fromJson` for what happens if it somehow does not.
+ pub const max_bytes = 256;
+
+ /// Read the `faixa` block off the JSON body of a 404, packing its strings
+ /// into `buffer` in `text_fields` order.
+ ///
+ /// Returns null — never an error — when the block is missing, null, not an
+ /// object, carries no `localidade`, or would not fit in `buffer`. The
+ /// answer is already `error.NotFound`, and no shape of this field may turn
+ /// that into a failure to parse. The API **omits** `faixa` rather than
+ /// sending null when the CEP falls in a band no municipality owns, so
+ /// absent is the common case, not a malformed one.
+ ///
+ /// Nothing is allocated. The result borrows `buffer`, which must be at
+ /// least `max_bytes` long to be sure of holding any real range.
+ pub fn fromJson(value: std.json.Value, buffer: []u8) ?Faixa {
+ const object = switch (value) {
+ .object => |o| o,
+ else => return null,
+ };
+ const block = switch (object.get("faixa") orelse return null) {
+ .object => |o| o,
+ else => return null,
+ };
+ const wrapped: std.json.Value = .{ .object = block };
+
+ var scratch: [text_fields.len][32]u8 = undefined;
+ var values: [text_fields.len][]const u8 = undefined;
+ var total: usize = 0;
+ inline for (text_fields, 0..) |name, i| {
+ values[i] = jsonText(wrapped, name, &scratch[i]);
+ total += values[i].len;
+ }
+
+ // Without the municipality the block names nothing, which is the same
+ // as not being there.
+ if (values[0].len == 0) return null;
+ if (total > buffer.len) return null;
+
+ var faixa: Faixa = .{ .ceps = jsonCount(block.get("ceps")) };
+ var cursor: usize = 0;
+ inline for (text_fields, 0..) |name, i| {
+ const v = values[i];
+ @memcpy(buffer[cursor..][0..v.len], v);
+ @field(faixa, name) = buffer[cursor..][0..v.len];
+ cursor += v.len;
+ }
+ return faixa;
+ }
+};
+
/// The response of `GET /health`.
///
/// Good for two things: checking that the API answers, and knowing **which
@@ -510,6 +626,86 @@ test "formattedCep passes a malformed CEP through" {
try testing.expectEqualStrings("123", address.formattedCep(&buffer));
}
+const areal_404 =
+ \\{"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE",
+ \\ "faixa":{"localidade":"Areal","uf":"RJ","estado":"Rio de Janeiro",
+ \\ "regiao":"Sudeste","ibge":"3300225","cep_ini":"25845001",
+ \\ "cep_fim":"25849899","ceps":118}}
+;
+
+test "Faixa.fromJson names the municipality of a CEP that does not exist" {
+ const parsed = try parseValue(areal_404);
+ defer parsed.deinit();
+
+ var buffer: [Faixa.max_bytes]u8 = undefined;
+ const faixa = Faixa.fromJson(parsed.value, &buffer).?;
+
+ try testing.expectEqualStrings("Areal", faixa.localidade);
+ try testing.expectEqualStrings("RJ", faixa.uf);
+ try testing.expectEqualStrings("Rio de Janeiro", faixa.estado);
+ try testing.expectEqualStrings("Sudeste", faixa.regiao);
+ try testing.expectEqualStrings("3300225", faixa.ibge);
+ // The DNE's own start for Areal is 25845000; cep_ini is the smallest CEP
+ // that exists, which is not the same number.
+ try testing.expectEqualStrings("25845001", faixa.cep_ini);
+ try testing.expectEqualStrings("25849899", faixa.cep_fim);
+ // 118 CEPs over the ~4,900 numbers the pair spans: the range is sparse.
+ try testing.expectEqual(@as(u64, 118), faixa.ceps);
+}
+
+test "a faixa the API omitted is null, not an error" {
+ const bodies = [_][]const u8{
+ \\{"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE"}
+ ,
+ \\{"erro":"cep_nao_encontrado","faixa":null}
+ ,
+ \\{"erro":"cep_nao_encontrado","faixa":{}}
+ ,
+ \\{"erro":"cep_nao_encontrado","faixa":"Areal"}
+ ,
+ \\{"erro":"cep_nao_encontrado","faixa":{"uf":"RJ","ceps":118}}
+ ,
+ \\{"erro":"cep_nao_encontrado","faixa":{"localidade":"","uf":"RJ"}}
+ ,
+ \\[]
+ ,
+ };
+ for (bodies) |body| {
+ const parsed = try parseValue(body);
+ defer parsed.deinit();
+
+ var buffer: [Faixa.max_bytes]u8 = undefined;
+ try testing.expect(Faixa.fromJson(parsed.value, &buffer) == null);
+ }
+}
+
+test "a partial faixa keeps what it has" {
+ const body =
+ \\{"faixa":{"localidade":"Areal","ceps":"118"}}
+ ;
+ const parsed = try parseValue(body);
+ defer parsed.deinit();
+
+ var buffer: [Faixa.max_bytes]u8 = undefined;
+ const faixa = Faixa.fromJson(parsed.value, &buffer).?;
+
+ try testing.expectEqualStrings("Areal", faixa.localidade);
+ try testing.expectEqualStrings("", faixa.uf);
+ try testing.expectEqualStrings("", faixa.cep_ini);
+ try testing.expectEqual(@as(u64, 118), faixa.ceps);
+}
+
+test "a faixa that does not fit is reported as absent, not truncated" {
+ const body =
+ \\{"faixa":{"localidade":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","uf":"RJ"}}
+ ;
+ const parsed = try parseValue(body);
+ defer parsed.deinit();
+
+ var small: [8]u8 = undefined;
+ try testing.expect(Faixa.fromJson(parsed.value, &small) == null);
+}
+
test "Health.fromJson reads counts and status" {
const body =
\\{"status":"ok","dne":"26071","ceps":1596168,"geo":894489,
diff --git a/src/root.zig b/src/root.zig
index 0a83c5c..d97c8ea 100644
--- a/src/root.zig
+++ b/src/root.zig
@@ -44,6 +44,10 @@
//! };
//! ```
//!
+//! After `error.NotFound`, `diag.faixa()` may name the municipality the CEP
+//! would have belonged to. It is context for the failure and never an address —
+//! see `Faixa`.
+//!
//! 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 the JSON is what you access on the value.
@@ -77,6 +81,7 @@ pub const Diagnostics = errors.Diagnostics;
pub const isRetriable = errors.isRetriable;
pub const Address = model.Address;
+pub const Faixa = model.Faixa;
pub const Geo = model.Geo;
pub const Health = model.Health;