feat: add getStateByCep and getMunicipalityByCep, the state and municipality that own a CEP range - #562
feat: add getStateByCep and getMunicipalityByCep, the state and municipality that own a CEP range#562hyanmandian wants to merge 4 commits into
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (6)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review. 📝 WalkthroughWalkthroughChangesCEP state lookup
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant Caller
participant getStateByCep
participant CEP_RANGES
participant DATA
Caller->>getStateByCep: Submit string or number CEP
getStateByCep->>CEP_RANGES: Match the parsed CEP
CEP_RANGES-->>getStateByCep: Return StateCode or no match
getStateByCep->>DATA: Resolve state metadata
DATA-->>getStateByCep: Return state data
getStateByCep-->>Caller: Return cloned State or null
Suggested reviewers: Merge Risk: ⚪ Minimal · up to The new offline CEP lookup maps supported ranges to state data and returns null for invalid or uncovered values; its public API and documentation are aligned, so it is ready to merge. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Tree-shaking report✅ No size regression. 2 new out of 164 exports.
What changed (2)
All exports (164)
How this is measuredEvery export is imported alone into an esbuild consumer bundle (minified, tree-shaken) built from the head and from the base of this pull request; the sizes are the resulting bundles, gzip is their gzipped size. 🔴 marks a regression: a pre-existing export that grew more than 20% and more than 256 B, or the bundle importing every pre-existing export growing more than 5%. 🟡 is growth under the threshold, 🟢 a decrease, ⚪ no change, 🆕 an export that does not exist on the base (never a regression), 🗑️ an export that was removed. An intentional increase is accepted with the |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## claude/get-cnpj-info #562 +/- ##
======================================================
Coverage 100.00% 100.00%
======================================================
Files 192 195 +3
Lines 2101 2112 +11
Branches 619 622 +3
======================================================
+ Hits 2101 2112 +11
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
…range Knowing the state of a CEP so far took a network call to a CEP API. The Correios assign every state one or two ranges of CEPs, so the state can be answered offline from a 30 row table: Amazonas, Distrito Federal and Goiás have two ranges each, and 00000-000 to 00999-999 and 78900-000 to 78999-999 belong to no state and answer null. The value goes through isValidCep and parseCep, and a number has to be a non-negative integer, as in getStateByIbgeCode. The result is the same State object the other state utils return.
The lookup walked the 27 states and re-scanned the 30 ranges for each one, up to 810 comparisons for every call. The question is which range holds the CEP, so the range table is the outer loop: at most 57 comparisons, and the shape reads like the sibling getStateByIbgeCode. Also say in the docs that a range is the block the state owns and not a promise that every CEP in it is in use, since 10000-000 to 10999-999 sits unused inside the range of São Paulo, and cover that block and the shape of the table (ascending, no overlap, one inner gap) with tests.
8c13c81 to
2b00546
Compare
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
commit: |
|
@coderabbitai review |
✅ Action performedReview finished.
|
…lity that owns a CEP range Adds an offline lookup of the municipality (city) a CEP belongs to, next to the getStateByCep this branch already adds, so the two compose: getStateByCep answers the UF, getMunicipalityByCep the município. The 5574 row range table is generated by scripts/municipality-cep-ranges.ts from a community CSV mirror of the Correios "Busca Faixa de CEP" search, joined by name against the IBGE municipalities this package already ships; the generator throws if any of the CSV's 5764 municipality rows fails to resolve to a known IBGE code, so a future refresh can never silently drop one. 24 of the CSV's municipality names do not match the IBGE spelling verbatim (diacritics, hyphenation, or a genuine historical rename such as Augusto Severo -> Campo Grande/RN). Their IBGE-vs-source spellings are curated as OTHER_NAMES, a new sparse table in _internals/constants/municipalities.ts keyed by IBGE code, so the generator can resolve them by name without duplicating the canonical name IBGE already publishes. One real municipality (Boa Esperança do Norte/MT, a district of Sorriso emancipated by the STF only in January 2025) has no row in the source under any spelling and is left without a range, same as getStateByCep already leaves the 78900-78999 gap. getStateByCep and getMunicipalityByCep shared their validate-parse-find logic almost verbatim, so it moves into a new internal, findCepRange, that both now call. Also renamed the internal municipalities constants file from cities.ts to municipalities.ts: it exports Municipality, not City, and nothing public imports the path directly, so the rename is not a breaking change. getCities, getMunicipalities and getMunicipality keep their names; a prior commit already deprecated getCities and the name-matching half of getMunicipality in favour of the municipality family, and that decision is unchanged here. Docs now say "municipality (city)" / "município (cidade)" on first mention in the municipality family, since "city" is the word a reader searches for, without renaming any export.
|
@coderabbitai review |
|
Both getting-started guides said 5573 Correios CEP ranges; the table scripts/municipality-cep-ranges.ts actually generates holds 5574 entries, matching the count already used in the PR description and in docs/utilities.md's prose.
What
Two offline CEP range lookups, plus one naming cleanup:
getStateByCep: the state (UF) that owns a CEP, from the CEP ranges the Correios assign to each state. A 30 row pure literal insrc/get-state-by-cep/constants.ts.getMunicipalityByCep: the municipality (city) that owns a CEP, from the CEP ranges the Correios assign to each municipality. A 5,574 row pure literal insrc/get-municipality-by-cep/constants.ts, generated byscripts/municipality-cep-ranges.tsand holding only the range and the 7-digit IBGE code, never a name:getMunicipalityByCodesupplies the name and state from the municipality table this package already ships.src/_internals/constants/cities.tstomunicipalities.ts: it exportsMunicipality, notCity, and nothing public imports the path directly, so this is not a breaking change. No public export is renamed.No network call in either lookup: both answer from a range table, not a CEP API.
API
isValidCepaccepts: 8 digits, string or number, with spaces, dots and hyphens ignored. A number must be a non-negative integer (isLookupCode), so-20040020and2004002.5arenullinstead of being read as a CEP. A CEP starting with0has to be a string, as inisValidCep.getStateByCepreturns the sameStateobject asgetStates/getStateByIbgeCode(a fresh copy).getMunicipalityByCepreturns the sameMunicipalityobject asgetMunicipalityByCode({ code, name, stateCode }, a fresh copy), so both compose with the rest of the state/municipality family.nullfor an invalid CEP, for any non string/number input, and for a CEP outside every range.getStateByCepandgetMunicipalityByCepshared their validate-parse-find logic almost verbatim, so it was extracted into a new internal,findCepRange(src/_internals/find-cep-range/find-cep-range.ts), that both now call;check:duplicationis what caught the near-duplicate.Ranges in the
getStateByCeptable (three states have two):Sources
getStateByCep
22930-869printed under Espírito Santo). It confirms, among others, the Goiás municipalities around Brasília at72800-001and up, Distrito Federal up to73405-999, Mato Grosso up to78899-999and Rondônia from76800-001.getMunicipalityByCep
getMunicipalityByCepsub-section below for what it confirms about this table.UF, CIDADE, CEP DE, CEP ATÉ, pinned to a specific revision of the gist so a re-run of the generator always reads the exact file this table was checked against). The gist declares no license or provenance, so it is cited asBased on:, neverOfficial:.The 30
getStateByCepboundaries are cross-verifiedThey no longer rest on the
tamnilgist alone. Every boundary was re-derived from sources independent of it, and all 30 match the table:72774-999against GO72800-001, GO72979-999against DF73000-001, MT78899-999).hugosenariCEP-range CSV this PR adds as the source forgetMunicipalityByCep(5,764 rows, one per municipality): mapping both endpoints of every one of those 5,764 rows through this 30 row UF table gives 0 mismatches over 11,528 endpoints, and the CSV pins each of the 30 boundaries to the adjacent CEP the same way the PDF does (for example PA68899-999against AP68900-001). This is an independent corroboration of the same kind the original review round used, from a source this PR already has to trust forgetMunicipalityByCep.69399-000Cantá/RR and69400-970Manacapuru/AM,76801-000Porto Velho/RO,73700-000Padre Bernardo/GO,72800-010Luziânia/GO,68890-000Afuá/PA,68900-010Macapá/AP,79002-000Campo Grande/MS.JoseQuintas/sefazclass(json/sefazcepuf.json),klawdyo/validation-br(src/cep.ts),pdrodavi/cep2uf, and the carrier and e-commerce tables of bring.com.br, blog.shoppub.com.br and ajuda.lojaintegrada.com.br.00000-000to00999-999and78900-000to78999-999).Two secondary sources disagree, and both are wrong:
78899-999, not78999-999. The Wikipedia zone table lists "MT Interior | 78110 - 78999", which would close the789xxgap. The official PDF's highest MT CEP is78899-999(Sorriso) and it lists no CEP at all in789xx; thehugosenariCSV has 0 of its 5,574 municipality ranges there either; live ViaCEP (checked again for this PR) answers{"erro":true}for78900-000,78950-000and78999-000.789xxis the range Rondônia vacated when it was moved to768xx, which is the same move that took Goiás down to76799-999. The gap stays.datasets-br/state-codesrecords SP as01000-000–09999-999plus11000-000–19999-999. The Correios UF faixa, which is what this table copies, gives SP a single01000-000–19999-999, and so does every other source checked.getStateByCep("10000-000")therefore answers SP although no city uses10xxx: thehugosenariCSV's two São Paulo capital ranges (01000-001–05999-999and08000-000–08499-999) both skip over10xxx, the "Localidades alvo" PDF has no CEP starting with10anywhere in its 76 pages, and live ViaCEP answers{"erro":true}for10000-000. A faixa is the block the state owns, not a guarantee that every CEP in it is in use, and SP has other unused blocks. The JSDoc and both docs say so.getMunicipalityByCep: the join and its cross-check
The generator (
scripts/municipality-cep-ranges.ts) reads the 5,764 municipality rows of thehugosenariCSV and resolves each one to an IBGE code by normalized name + UF againstgetMunicipalities()'s own table:normalizeMunicipalityName: accents and case folded).Dona EusébiavsDona Euzébia,São Thomé das LetrasvsSão Tomé das Letras,Santo Antônio do LevergervsSanto Antônio de Leverger, and 18 more of the same kind), plus two genuine historical renames confirmed against independent sources:Augusto Severo(RN) isCampo Grande's name from 1903 until a 2018 law and a 2019 plebiscite reverted it (Wikipedia, TRE-RN), andBoa Saúde(RN) isJanuário Cicco's name before a 1991 reversion (IBGE Cidades). These 24 are curated asOTHER_NAMES(see below) so the generator resolves all 5,764 rows; it throws if a future refresh reintroduces an unresolved name.5101837), a district of Sorriso whose emancipation the STF only validated in January 2025. It is simply absent from the CSV, not misspelled, so no alias fixes it;getMunicipalityByCepreturnsnullfor its real CEPs, the same waygetStateByCepreturnsnullfor its documented78900–78999gap.I did not have a way to run the official "Busca Faixa de CEP" per-municipality search (same CAPTCHA as
getStateByCep) or to open all 76 pages of the "Localidades alvo" PDF municipality by municipality against all 5,574 ranges in the time available. What I did verify against that PDF: it lists no CEP in the789xxMT gap or the10xxxSP gap (both above), and its municipality lists for the UFs I spot-checked (MT, DF, GO-entorno) are consistent with the ranges this table assigns them. A full per-municipality reconciliation against the PDF is an open point below.Naming:
getCities/getMunicipalityvs the municipality familyLooked at
git log, the JSDoc of both functions andCONTRIBUTING.md; there is noCONTRIBUTING.mdor code mention of a planned rename of any public export. Commitd313bcc2(docs(municipalities): deprecate getCities and getMunicipality in favour of the municipality family) already settled this onmain:So: no public export is renamed in this PR (nor should it be, that would be the exact breaking change the release must avoid). What did move: the internal
_internals/constants/cities.tstomunicipalities.ts, since it exportsMunicipality, is behindgetMunicipalities/getMunicipalityByCode/getMunicipality/getCities/getMunicipalityByCepalike, and nothing public imports its path. Docs now say "municipality (city)" / "município (cidade)" on the## States and municipalitiesheading and the first sentence ofgetMunicipalities/getMunicipalityByCode/getMunicipalityByCep, since "city"/"cidade" is what a reader searches for;getCities's own doc entry already says "city" throughout.OTHER_NAMES: alternative spellings in the municipality tableAdded
OTHER_NAMES, a new export ofsrc/_internals/constants/municipalities.ts,Readonly<Record<string, readonly string[]>>keyed by the 7-digit IBGE code:otherNames(as a per-code array) was the maintainer's suggestion; I kept it, exported asOTHER_NAMESto match theSCREAMING_CASEevery other top-level table in this file uses (DATA).DATA's[name, code]pairs.DATAstill has exactly one entry per municipality with no shape change, so the 5,547 municipalities with no variant cost nothing extra (no third array slot, no key, nothing to parse); only the 24 that need it get an entry. A 2- or 3-tuple union onDATAitself was the alternative, but it would touch and re-emit all 5,571 rows on every generator run instead of 24, for the same result.OTHER_NAMESis curated insidescripts/cities.ts(the values, with a one-line citation of where each mismatch was found) and written intomunicipalities.tsby the same generator that writesDATA, which also fails the build if a curated code no longer matches a real IBGE municipality (OTHER_NAMES has codes IBGE no longer lists), so a future IBGE refresh can never leave a stale entry silently in place.getMunicipality/getMunicipalityByNamedo not match on it.getMunicipalityCodeByName(the name half of the deprecatedgetMunicipality) is explicitly being phased out in favour of applications matching overgetMunicipalities()themselves, per thed313bcc2decision quoted above ("names vary in ways no library rule settles"). Wiring a new matching capability into code the library is actively steering people away from would contradict that decision, so I left it untouched.OTHER_NAMESis consumed today only byscripts/municipality-cep-ranges.ts, to resolve the CEP CSV's spelling against the IBGE canonical one; it is real, statically-imported usage (knippasses), not dead weight, and is available for a future non-deprecated name-matching utility if one is ever added.Verification
npm run check: passnpm run test -- --run: 6434 passed, 3 skipped, 19 todonpm run test:coverage: 100% statements, branches, functions and linesnpm run build,npm run check:api:update: pass, report committed (two new exports:getMunicipalityByCep, andMunicipalityre-exported from its new path)npm run check:unused: pass (knip resolvesOTHER_NAMES's only consumer, the generator's dynamicimport(), as real usage)npm run check:duplication: 0 clones (after extractingfindCepRange; before the extraction, jscpd flaggedgetStateByCep/getMunicipalityByCepas a 6 line clone)npm run check:tree-shaking:getStateByCep4620 B / 1565 B gzip (up from 4581/1549 before thefindCepRangeextraction, well under the 20%/256 B regression threshold);getMunicipalityByCep402,787 B / 101,136 B gzip on its own, since it pulls in the full municipality table (shared withgetMunicipalityByCode) plus its own 5,574 row range tablenpm run check:commits: pass (commitlint --from origin/claude/get-state-by-cep --to HEAD)npm run test:mutation -- --mutate 'src/get-municipality-by-cep/get-municipality-by-cep.ts': 100% (1 killed, 0 survived)npm run test:mutation -- --mutate 'src/_internals/find-cep-range/find-cep-range.ts': 100% (17 killed, 0 survived)npm run test:mutation -- --mutate 'src/get-state-by-cep/get-state-by-cep.ts': 100% (9 killed, 0 survived, re-verified after the refactor)bun test src: 6434 pass, 0 fail;npm run test:deno: 6434 passed, 0 failednpm run build:docsandnpm run build:jsr: run, output committed (jsr.jsongains./get-municipality-by-cep;docs/getting-started.mdanddocs/pt-br/getting-started.mdbundle-size tables gain agetMunicipalityByCeprow and refresh thegetCitiesrow to the currently measured size)Open points
getStateByCepandgetMunicipalityByCepranges were not read from the official Correios search itself, because it is behind a CAPTCHA that I did not try to bypass. They come from third party copies of that search, corroborated as described above.getMunicipalityByCep's 5,574 ranges were cross-checked against the official "Localidades alvo" PDF only for the disputed MT/SP gaps and a few spot-checked UFs (MT, DF, GO-entorno), not municipality by municipality across all 76 pages; a maintainer or a follow-up pass could extend that.getMunicipalityByCepreturnsnullfor its real CEPs. This is a gap in the source, not a bug; flagged in the JSDoc.src/get-state-by-cep/constants.ts, with no generator underscripts/, because the only official source cannot be fetched by a script;getMunicipalityByCep's table, by contrast, does have a generator, because itsBased on:mirror is fetchable over plain HTTPS.66000-000to68899-999); the task mentioned it as historically split, and no source checked lists more than one range for it.isValidCepreads-20040020and2004002.5as valid CEPs today; bothgetStateByCepandgetMunicipalityByCepreject them as numbers (they stay accepted as strings such as"20040-020"). That util was left untouched.Rebase onto #560
Rebased from
mainontoclaude/get-cnpj-info, so this branch now carries #588, #558, #559 and #560 underneath it. Conflicts resolved:docs/llms.txtanddocs/llms-full.txtare no longer tracked (they are generated now), so both weregit rm-ed.getStateByCepsection ofdocs/utilities.mdanddocs/pt-br/utilities.mdwas ported into the new per-utility format of feat: Standard Schema wrapper, JSR, pkg.pr.new, docs previews and a playground #556: a short paragraph, a bullet list for the accepted input and the edge cases, thejavascriptblock, and aSource:line pointing at the Correios "Busca Faixa de CEP".src/index.tsandsrc/index.test.tskept strictly alphabetical, betweengetPixPayloadInfoandgetStateByIbgeCode(and, in the later commit, betweengetMunicipalityandgetMunicipalityByCode).jsr.json(new onmain) regenerated withnpm run build:jsr, andreports/api/brazilian-utils.api.mdwithnpm run check:api:update. Both are folded into the commits that own them, with no separate "regenerate" commit.Re-verified on the rebased branch:
npm run check,npm run test:coverage(100% statements, branches, functions and lines),npm run build,npm run check:unused,npm run check:duplicationandnpm run check:commitsall pass.Summary by CodeRabbit
New Features
nullfor invalid, uncovered, or unsupported ranges.Documentation