summaryrefslogtreecommitdiffstats
path: root/lib/cepis/faixa.ex
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-27 20:21:38 -0300
committerMurilo Ijanc <[email protected]>2026-07-27 20:21:38 -0300
commitf9f1d248f3a3a2b781507195e00527db1e39f8d3 (patch)
treef99b24f5923e436339b8e8ddb8af6ac854488439 /lib/cepis/faixa.ex
parent075683ff44a254e99adbad8a0372ef80ff06151a (diff)
downloadelixir-main.tar.gz
Carry the municipality 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, most often the -000 that 957 municipalities never received. The API now answers 404 with an optional faixa naming the municipality that number would have belonged to, so the caller can say "you are in Areal, but not at that number" instead of a dead end. It stays an error. Cepis.Faixa hangs off Cepis.NotFoundError as context for the failure, never as an address, and parsing is deliberately tolerant: an absent, truncated or unreadable faixa yields nil rather than costing the caller a clean definitive answer. The bounds are the smallest and largest CEP that actually exist there, not the range the Correios publish, and almost nothing between them is valid — hence ceps and density/1, so nobody reads the pair as a list of numbers to try.
Diffstat (limited to 'lib/cepis/faixa.ex')
-rw-r--r--lib/cepis/faixa.ex124
1 files changed, 124 insertions, 0 deletions
diff --git a/lib/cepis/faixa.ex b/lib/cepis/faixa.ex
new file mode 100644
index 0000000..ab85e8d
--- /dev/null
+++ b/lib/cepis/faixa.ex
@@ -0,0 +1,124 @@
+defmodule Cepis.Faixa do
+ @moduledoc """
+ The municipality a CEP that does **not** exist would have belonged to.
+
+ It arrives on `Cepis.NotFoundError`, and nowhere else. **It is context for an
+ error, not an address**: the lookup failed, the CEP is not in the DNE, and
+ nothing here says otherwise. Using it as a fallback address is the one wrong
+ use.
+
+ It is worth having because a failing CEP is rarely random — it is almost
+ always someone who knows the city and guessed the number, most often the
+ `-000`, which 957 municipalities never received. Naming the municipality
+ turns "does not exist" into "you are in Areal, but not at that number", which
+ is the difference between a dead end and a form the user can fix.
+
+ **Field names are the API's own, in Portuguese**, as everywhere else in this
+ library: `localidade`, `uf`, `estado`, `regiao`, `ibge`, `cep_ini`,
+ `cep_fim`, `ceps`.
+
+ > #### The range is sparse {: .warning}
+ >
+ > `cep_ini` and `cep_fim` are the smallest and the largest CEP that
+ > **actually exist** in that municipality — not the bounds the Correios
+ > publish. The DNE's own start for Areal is `25845000`, which is exactly the
+ > CEP that just failed; echoing it back would be useless. The numbers between
+ > the two bounds are mostly not CEPs at all: Areal's 118 are scattered across
+ > thousands of them. That is what `ceps` is for, and what `density/1`
+ > measures. Never expand the pair into a list of valid CEPs.
+
+ The API **omits** `faixa`, never sends it as null, when the CEP falls in a
+ numeric band no municipality owns. So `nil` is an ordinary answer, not a
+ failure, and every caller has to handle it.
+ """
+
+ defstruct localidade: "",
+ uf: "",
+ estado: "",
+ regiao: "",
+ ibge: "",
+ cep_ini: "",
+ cep_fim: "",
+ ceps: 0
+
+ @type t :: %__MODULE__{
+ localidade: String.t(),
+ uf: String.t(),
+ estado: String.t(),
+ regiao: String.t(),
+ ibge: String.t(),
+ cep_ini: String.t(),
+ cep_fim: String.t(),
+ ceps: non_neg_integer()
+ }
+
+ @doc """
+ The share of the numbers between `cep_ini` and `cep_fim` that are real CEPs.
+
+ About `0.024` for Areal — 118 CEPs over the 4899 numbers between `25845001`
+ and `25849899`. It exists so the sparsity is measurable instead of being a
+ footnote nobody reads before iterating over the interval:
+
+ if Cepis.Faixa.density(faixa) < 0.5 do
+ # guessing another number in the range is not going to work
+ suggest_search_by_street(faixa.localidade, faixa.uf)
+ end
+
+ Returns `0.0` when the bounds are not two readable numbers.
+ """
+ @spec density(t()) :: float()
+ def density(%__MODULE__{ceps: ceps, cep_ini: cep_ini, cep_fim: cep_fim}) do
+ with {first, ""} <- Integer.parse(cep_ini),
+ {last, ""} <- Integer.parse(cep_fim),
+ true <- last >= first do
+ ceps / (last - first + 1)
+ else
+ _ -> 0.0
+ end
+ end
+
+ @doc false
+ @spec from_map(term()) :: t() | nil
+ def from_map(%{"localidade" => localidade} = data)
+ when is_binary(localidade) and localidade != "" do
+ %__MODULE__{
+ localidade: localidade,
+ uf: text(data, "uf"),
+ estado: text(data, "estado"),
+ regiao: text(data, "regiao"),
+ ibge: text(data, "ibge"),
+ cep_ini: text(data, "cep_ini"),
+ cep_fim: text(data, "cep_fim"),
+ ceps: count(data, "ceps")
+ }
+ end
+
+ def from_map(_), do: nil
+
+ defp text(data, key) do
+ case Map.get(data, key) do
+ value when is_binary(value) -> value
+ value when is_number(value) -> to_string(value)
+ _ -> ""
+ end
+ end
+
+ defp count(data, key) do
+ case Map.get(data, key) do
+ value when is_integer(value) ->
+ value
+
+ value when is_float(value) ->
+ trunc(value)
+
+ value when is_binary(value) ->
+ case Integer.parse(value) do
+ {number, _} -> number
+ :error -> 0
+ end
+
+ _ ->
+ 0
+ end
+ end
+end