Skip to content

feat: add getStateByCep and getMunicipalityByCep, the state and municipality that own a CEP range - #562

Open
hyanmandian wants to merge 4 commits into
claude/get-cnpj-infofrom
claude/get-state-by-cep
Open

hyanmandian wants to merge 4 commits into
claude/get-cnpj-infofrom
claude/get-state-by-cep

Conversation

@hyanmandian

@hyanmandian hyanmandian commented Sep 19, 2026

Copy link
Copy Markdown
Member

Stacked on #560. This PR sits on top of #560 (getCnpjInfo) and merges after it, which in turn sits on #559, #558 and #588. Its base branch is claude/get-cnpj-info, so the diff shown here is the getStateByCep / getMunicipalityByCep changes alone. Part of stack #591.

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 in src/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 in src/get-municipality-by-cep/constants.ts, generated by scripts/municipality-cep-ranges.ts and holding only the range and the 7-digit IBGE code, never a name: getMunicipalityByCode supplies the name and state from the municipality table this package already ships.
  • Renamed the internal src/_internals/constants/cities.ts to municipalities.ts: it exports Municipality, not City, 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

getStateByCep(value: string | number): State | null
getMunicipalityByCep(value: string | number): Municipality | null
  • Both accept what isValidCep accepts: 8 digits, string or number, with spaces, dots and hyphens ignored. A number must be a non-negative integer (isLookupCode), so -20040020 and 2004002.5 are null instead of being read as a CEP. A CEP starting with 0 has to be a string, as in isValidCep.
  • getStateByCep returns the same State object as getStates / getStateByIbgeCode (a fresh copy). getMunicipalityByCep returns the same Municipality object as getMunicipalityByCode ({ code, name, stateCode }, a fresh copy), so both compose with the rest of the state/municipality family.
  • null for an invalid CEP, for any non string/number input, and for a CEP outside every range. getStateByCep and getMunicipalityByCep shared 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:duplication is what caught the near-duplicate.
getStateByCep("01310-100"); // { code: "SP", name: "São Paulo", regionCode: "SE", regionName: "Sudeste", ibgeCode: 35 }
getStateByCep(20040020)?.code; // "RJ"
getStateByCep("69300-000")?.code; // "RR" (inside the Amazonas block)
getStateByCep("72800-000")?.code; // "GO" (inside the Distrito Federal block)
getStateByCep("78900-000"); // null (no state owns 78900-000 to 78999-999)

getMunicipalityByCep("01310-100"); // { code: "3550308", name: "São Paulo", stateCode: "SP" }
getMunicipalityByCep(20040020); // { code: "3304557", name: "Rio de Janeiro", stateCode: "RJ" }
getMunicipalityByCep("00999-999"); // null

Ranges in the getStateByCep table (three states have two):

UF Range UF Range
SP 01000-000 to 19999-999 AM 69000-000 to 69299-999 and 69400-000 to 69899-999
RJ 20000-000 to 28999-999 RR 69300-000 to 69399-999
ES 29000-000 to 29999-999 AC 69900-000 to 69999-999
MG 30000-000 to 39999-999 DF 70000-000 to 72799-999 and 73000-000 to 73699-999
BA 40000-000 to 48999-999 GO 72800-000 to 72999-999 and 73700-000 to 76799-999
SE 49000-000 to 49999-999 RO 76800-000 to 76999-999
PE 50000-000 to 56999-999 TO 77000-000 to 77999-999
AL 57000-000 to 57999-999 MT 78000-000 to 78899-999
PB 58000-000 to 58999-999 MS 79000-000 to 79999-999
RN 59000-000 to 59999-999 PR 80000-000 to 87999-999
CE 60000-000 to 63999-999 SC 88000-000 to 89999-999
PI 64000-000 to 64999-999 RS 90000-000 to 99999-999
MA 65000-000 to 65999-999 PA 66000-000 to 68899-999 (a single range today)
AP 68900-000 to 68999-999

Sources

getStateByCep

getMunicipalityByCep

  • Official: https://buscacepinter.correios.com.br/app/faixa_cep_uf_localidade/index.php (Correios "Busca Faixa de CEP"; a search by municipality answers its range). Same CAPTCHA, same limitation as above.
  • Official cross-check: the same "Localidades alvo" PDF above; see the getMunicipalityByCep sub-section below for what it confirms about this table.
  • Based on: https://gist.github.com/hugosenari/ec1a7d88f5bdd01844424dbc9aff9590, a CSV mirror of the same kind of Correios answer, one row per municipality (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 as Based on:, never Official:.

The 30 getStateByCep boundaries are cross-verified

They no longer rest on the tamnil gist alone. Every boundary was re-derived from sources independent of it, and all 30 match the table:

  • The official "Localidades alvo" PDF above, re-extracted independently: same 10,189 CEPs, same single typo, no contradiction. It pins most boundaries tightly (for example DF 72774-999 against GO 72800-001, GO 72979-999 against DF 73000-001, MT 78899-999).
  • The hugosenari CEP-range CSV this PR adds as the source for getMunicipalityByCep (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 PA 68899-999 against AP 68900-001). This is an independent corroboration of the same kind the original review round used, from a source this PR already has to trust for getMunicipalityByCep.
  • Live ViaCEP lookups (a DNE mirror) on the boundaries the PDF leaves loose: 69399-000 Cantá/RR and 69400-970 Manacapuru/AM, 76801-000 Porto Velho/RO, 73700-000 Padre Bernardo/GO, 72800-010 Luziânia/GO, 68890-000 Afuá/PA, 68900-010 Macapá/AP, 79002-000 Campo Grande/MS.
  • Five further independent range tables agree on all 30 rows: 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.
  • The table has no overlap and exactly the two gaps it documents (00000-000 to 00999-999 and 78900-000 to 78999-999).

Two secondary sources disagree, and both are wrong:

  • MT ends at 78899-999, not 78999-999. The Wikipedia zone table lists "MT Interior | 78110 - 78999", which would close the 789xx gap. The official PDF's highest MT CEP is 78899-999 (Sorriso) and it lists no CEP at all in 789xx; the hugosenari CSV has 0 of its 5,574 municipality ranges there either; live ViaCEP (checked again for this PR) answers {"erro":true} for 78900-000, 78950-000 and 78999-000. 789xx is the range Rondônia vacated when it was moved to 768xx, which is the same move that took Goiás down to 76799-999. The gap stays.
  • SP is one range, not two. datasets-br/state-codes records SP as 01000-00009999-999 plus 11000-00019999-999. The Correios UF faixa, which is what this table copies, gives SP a single 01000-00019999-999, and so does every other source checked. getStateByCep("10000-000") therefore answers SP although no city uses 10xxx: the hugosenari CSV's two São Paulo capital ranges (01000-00105999-999 and 08000-00008499-999) both skip over 10xxx, the "Localidades alvo" PDF has no CEP starting with 10 anywhere in its 76 pages, and live ViaCEP answers {"erro":true} for 10000-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 the hugosenari CSV and resolves each one to an IBGE code by normalized name + UF against getMunicipalities()'s own table:

  • 5,740 rows match an IBGE name exactly (after normalizeMunicipalityName: accents and case folded).
  • 24 rows do not match verbatim. All 24 are the same município under a different spelling, not a different place: diacritic or hyphenation variants (Dona Eusébia vs Dona Euzébia, São Thomé das Letras vs São Tomé das Letras, Santo Antônio do Leverger vs Santo Antônio de Leverger, and 18 more of the same kind), plus two genuine historical renames confirmed against independent sources: Augusto Severo (RN) is Campo Grande's name from 1903 until a 2018 law and a 2019 plebiscite reverted it (Wikipedia, TRE-RN), and Boa Saúde (RN) is Januário Cicco's name before a 1991 reversion (IBGE Cidades). These 24 are curated as OTHER_NAMES (see below) so the generator resolves all 5,764 rows; it throws if a future refresh reintroduces an unresolved name.
  • 1 IBGE municipality has no range at all under any spelling: Boa Esperança do Norte (MT, IBGE code 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; getMunicipalityByCep returns null for its real CEPs, the same way getStateByCep returns null for its documented 7890078999 gap.
  • The 5,744 + 24 = 5,768 resolved rows collapse to 5,574 ranges after merging the ranges of a municipality that touch or overlap (the CSV lists a handful of municipalities twice, once per historical name, under the same or an adjacent range), covering 5,570 of the 5,571 IBGE municipalities.

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 the 789xx MT gap or the 10xxx SP 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/getMunicipality vs the municipality family

Looked at git log, the JSDoc of both functions and CONTRIBUTING.md; there is no CONTRIBUTING.md or code mention of a planned rename of any public export. Commit d313bcc2 (docs(municipalities): deprecate getCities and getMunicipality in favour of the municipality family) already settled this on main:

The municipality is the entity of the Constituição (art. 18) and of the IBGE dataset the library ships, so getMunicipalities and getMunicipalityByCode are the API. getCities (names only) and the asynchronous getMunicipality of 2.3.0 keep working unchanged and are marked deprecated, to be removed in the next major; matching a municipality by name, which getMunicipality also did, is left to the application over getMunicipalities, since names vary in ways no library rule settles (abbreviations, former names, hyphens, typos).

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.ts to municipalities.ts, since it exports Municipality, is behind getMunicipalities/getMunicipalityByCode/getMunicipality/getCities/getMunicipalityByCep alike, and nothing public imports its path. Docs now say "municipality (city)" / "município (cidade)" on the ## States and municipalities heading and the first sentence of getMunicipalities/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 table

Added OTHER_NAMES, a new export of src/_internals/constants/municipalities.ts, Readonly<Record<string, readonly string[]>> keyed by the 7-digit IBGE code:

export const OTHER_NAMES: Readonly<Record<string, readonly string[]>> = {
  "2401305": ["Augusto Severo"], // Campo Grande/RN, its name 1903-2019
  // ...24 entries total
};
  • Name: otherNames (as a per-code array) was the maintainer's suggestion; I kept it, exported as OTHER_NAMES to match the SCREAMING_CASE every other top-level table in this file uses (DATA).
  • Shape: a sparse companion table, not a third tuple element on DATA's [name, code] pairs. DATA still 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 on DATA itself 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.
  • Generated, not hand-edited: OTHER_NAMES is curated inside scripts/cities.ts (the values, with a one-line citation of where each mismatch was found) and written into municipalities.ts by the same generator that writes DATA, 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/getMunicipalityByName do not match on it. getMunicipalityCodeByName (the name half of the deprecated getMunicipality) is explicitly being phased out in favour of applications matching over getMunicipalities() themselves, per the d313bcc2 decision 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_NAMES is consumed today only by scripts/municipality-cep-ranges.ts, to resolve the CEP CSV's spelling against the IBGE canonical one; it is real, statically-imported usage (knip passes), not dead weight, and is available for a future non-deprecated name-matching utility if one is ever added.

Verification

  • npm run check: pass
  • npm run test -- --run: 6434 passed, 3 skipped, 19 todo
  • npm run test:coverage: 100% statements, branches, functions and lines
  • npm run build, npm run check:api:update: pass, report committed (two new exports: getMunicipalityByCep, and Municipality re-exported from its new path)
  • npm run check:unused: pass (knip resolves OTHER_NAMES's only consumer, the generator's dynamic import(), as real usage)
  • npm run check:duplication: 0 clones (after extracting findCepRange; before the extraction, jscpd flagged getStateByCep/getMunicipalityByCep as a 6 line clone)
  • npm run check:tree-shaking: getStateByCep 4620 B / 1565 B gzip (up from 4581/1549 before the findCepRange extraction, well under the 20%/256 B regression threshold); getMunicipalityByCep 402,787 B / 101,136 B gzip on its own, since it pulls in the full municipality table (shared with getMunicipalityByCode) plus its own 5,574 row range table
  • npm 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 failed
  • npm run build:docs and npm run build:jsr: run, output committed (jsr.json gains ./get-municipality-by-cep; docs/getting-started.md and docs/pt-br/getting-started.md bundle-size tables gain a getMunicipalityByCep row and refresh the getCities row to the currently measured size)
  • Browser test scripts and the full Stryker run were not run locally; CI runs both.
  • The corroboration below relies only on sources this repository already cites (the official PDF, the CEP-range CSV this PR adds, live ViaCEP) and no longer on any third-party ERP dataset a previous revision of this PR body cited for the same purpose.

Open points

  • The getStateByCep and getMunicipalityByCep ranges 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.
  • One IBGE municipality, Boa Esperança do Norte/MT (emancipated 2025), has no CEP range in the source and so getMunicipalityByCep returns null for its real CEPs. This is a gap in the source, not a bug; flagged in the JSDoc.
  • The table is hand written in src/get-state-by-cep/constants.ts, with no generator under scripts/, because the only official source cannot be fetched by a script; getMunicipalityByCep's table, by contrast, does have a generator, because its Based on: mirror is fetchable over plain HTTPS.
  • Pará is a single range today (66000-000 to 68899-999); the task mentioned it as historically split, and no source checked lists more than one range for it.
  • isValidCep reads -20040020 and 2004002.5 as valid CEPs today; both getStateByCep and getMunicipalityByCep reject them as numbers (they stay accepted as strings such as "20040-020"). That util was left untouched.

Rebase onto #560

Rebased from main onto claude/get-cnpj-info, so this branch now carries #588, #558, #559 and #560 underneath it. Conflicts resolved:

  • docs/llms.txt and docs/llms-full.txt are no longer tracked (they are generated now), so both were git rm-ed.
  • The getStateByCep section of docs/utilities.md and docs/pt-br/utilities.md was 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, the javascript block, and a Source: line pointing at the Correios "Busca Faixa de CEP".
  • src/index.ts and src/index.test.ts kept strictly alphabetical, between getPixPayloadInfo and getStateByIbgeCode (and, in the later commit, between getMunicipality and getMunicipalityByCode).
  • jsr.json (new on main) regenerated with npm run build:jsr, and reports/api/brazilian-utils.api.md with npm 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:duplication and npm run check:commits all pass.

Summary by CodeRabbit

  • New Features

    • Added offline CEP lookups for Brazilian states and municipalities.
    • Supports formatted CEP strings and non-negative integer inputs, including punctuation.
    • Returns matching state or municipality details for recognized CEP ranges, and null for invalid, uncovered, or unsupported ranges.
    • Added both functions to the public package API.
  • Documentation

    • Added usage details and API references in English and Portuguese documentation.
    • Updated municipality terminology and bundle-size information.

@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: eb3d43bc-fb8c-4637-a7c5-a1c950f167b7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: f1e523bb-80c1-46e3-a559-2d22a49a7638

📥 Commits

Reviewing files that changed from the base of the PR and between 8c13c81 and 2b00546.

📒 Files selected for processing (6)
  • docs/pt-br/utilities.md
  • docs/utilities.md
  • jsr.json
  • reports/api/brazilian-utils.api.md
  • src/index.test.ts
  • src/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/index.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

Changes

CEP state lookup

Layer / File(s) Summary
Lookup ranges and implementation
src/get-state-by-cep/constants.ts, src/get-state-by-cep/get-state-by-cep.ts
Adds ordered Correios ranges and getStateByCep, which validates, parses, matches, and resolves CEP values to cloned state data.
Lookup validation and coverage tests
src/get-state-by-cep/get-state-by-cep.test.ts
Tests formats, boundaries, gaps, invalid values, defensive copying, property behavior, and TypeScript types.
Public API and documentation
src/index.ts, jsr.json, reports/api/brazilian-utils.api.md, src/index.test.ts, docs/utilities.md, docs/pt-br/utilities.md
Exports getStateByCep, updates the API surface, and documents its inputs, range behavior, and return values.

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
Loading

Suggested reviewers: claude

Merge Risk: ⚪ Minimal · up to 2b005

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)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title correctly mentions getStateByCep, but it also claims that getMunicipalityByCep was added. The changeset contains no municipality lookup. Remove getMunicipalityByCep and the municipality wording from the title, or include the corresponding implementation and related documentation if that feature is intended.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 5…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Tree-shaking report

No size regression. 2 new out of 164 exports.

Base Head Δ
Pre-existing exports, all imported 650.1 KB 650.1 KB (gzip 166.6 KB) +12 B (+0.0%)
Full import 650.1 KB 888.0 KB (gzip 216.5 KB) +237.9 KB (+36.6%)
Exports 162 164 +2

What changed (2)

Export Base Head Δ gzip
🆕 getMunicipalityByCep 393.3 KB new 98.8 KB
🆕 getStateByCep 4.5 KB new 1.5 KB
All exports (164)
Export Base Head Δ gzip
GetAddressInfoByCepError 966 B 966 B 0 B 600 B
GetAddressInfoByCepNotFoundError 1.0 KB 1.0 KB 0 B 618 B
GetAddressInfoByCepServiceError 1.0 KB 1.0 KB 0 B 617 B
GetAddressInfoByCepValidationError 1.0 KB 1.0 KB 0 B 620 B
GetCepInfoByAddressError 966 B 966 B 0 B 600 B
GetCepInfoByAddressNotFoundError 1.0 KB 1.0 KB 0 B 618 B
GetCepInfoByAddressValidationError 1.0 KB 1.0 KB 0 B 620 B
addBusinessDays 6.8 KB 6.8 KB 0 B 2.8 KB
capitalize 2.5 KB 2.5 KB 0 B 1.3 KB
convertCurrencyToWords 2.8 KB 2.8 KB 0 B 1.5 KB
convertDateToWords 3.2 KB 3.2 KB 0 B 1.7 KB
convertLicensePlateToMercosul 1.3 KB 1.3 KB 0 B 807 B
convertNumberToWords 2.4 KB 2.4 KB 0 B 1.3 KB
differenceInBusinessDays 6.9 KB 6.9 KB 0 B 2.9 KB
formatBoleto 1.4 KB 1.4 KB 0 B 837 B
formatCEP 1.2 KB 1.2 KB 0 B 777 B
formatCNPJ 1.4 KB 1.4 KB 0 B 854 B
formatCPF 1.3 KB 1.3 KB 0 B 806 B
formatCaepf 1.3 KB 1.3 KB 0 B 786 B
formatCei 1.3 KB 1.3 KB 0 B 785 B
formatCep 1.2 KB 1.2 KB 0 B 777 B
formatCertidao 1.3 KB 1.3 KB 0 B 789 B
formatCnae 1.2 KB 1.2 KB 0 B 781 B
formatCnh 1.3 KB 1.3 KB 0 B 780 B
formatCno 1.3 KB 1.3 KB 0 B 786 B
formatCnpj 1.4 KB 1.4 KB 0 B 854 B
formatCns 1.3 KB 1.3 KB 0 B 780 B
formatCpf 1.3 KB 1.3 KB 0 B 806 B
formatCurrency 1.8 KB 1.8 KB 0 B 1.0 KB
formatIban 1.1 KB 1.1 KB 0 B 696 B
formatLegalNature 1.2 KB 1.2 KB 0 B 777 B
formatLicensePlate 1.2 KB 1.2 KB 0 B 738 B
formatNcm 1.2 KB 1.2 KB 0 B 780 B
formatNfeKey 1.3 KB 1.3 KB 0 B 783 B
formatPassport 1.0 KB 1.0 KB 0 B 643 B
formatPhone 2.8 KB 2.8 KB 0 B 1.4 KB
formatPis 1.3 KB 1.3 KB 0 B 781 B
formatProcessoJuridico 1.3 KB 1.3 KB 0 B 785 B
formatSuframa 1.3 KB 1.3 KB 0 B 779 B
formatVoterId 1.3 KB 1.3 KB 0 B 821 B
generateBoleto 2.1 KB 2.1 KB 0 B 1.2 KB
generateCNPJ 1.6 KB 1.6 KB 0 B 967 B
generateCPF 1.4 KB 1.4 KB 0 B 878 B
generateCep 984 B 984 B 0 B 609 B
generateCnh 1.4 KB 1.4 KB 0 B 829 B
generateCnpj 1.6 KB 1.6 KB 0 B 967 B
generateCpf 1.4 KB 1.4 KB 0 B 878 B
generateLegalNature 5.9 KB 5.9 KB 0 B 2.1 KB
generateLicensePlate 1.1 KB 1.1 KB 0 B 692 B
generatePassport 1.1 KB 1.1 KB 0 B 655 B
generatePhone 1.5 KB 1.5 KB 0 B 900 B
generatePis 1.2 KB 1.2 KB 0 B 743 B
generatePixPayload 6.3 KB 6.3 KB 0 B 2.8 KB
generateProcessoJuridico 1.4 KB 1.4 KB 0 B 870 B
generateRenavam 1.2 KB 1.2 KB 0 B 760 B
generateSuframa 1.3 KB 1.3 KB 0 B 809 B
generateVoterId 1.7 KB 1.7 KB 0 B 1022 B
getAddressInfoByCep 4.1 KB 4.1 KB 0 B 1.9 KB
getAreaCodeInfo 3.9 KB 3.9 KB 0 B 1.4 KB
getAreaCodesByState 1.6 KB 1.6 KB 0 B 918 B
getBankByCode 38.6 KB 38.6 KB 0 B 9.8 KB
getBankByIspb 38.6 KB 38.6 KB 0 B 9.8 KB
getBanks 38.4 KB 38.4 KB 0 B 9.6 KB
getBoletoInfo 3.1 KB 3.1 KB 0 B 1.6 KB
getCbo 119.1 KB 119.1 KB 0 B 30.7 KB
getCepInfoByAddress 2.7 KB 2.7 KB 0 B 1.4 KB
getCertidaoInfo 1.8 KB 1.8 KB 0 B 1.0 KB
getCfop 68.9 KB 68.9 KB 0 B 6.9 KB
getCities 154.3 KB 154.3 KB 0 B 49.9 KB
getCnae 93.9 KB 93.9 KB 0 B 21.2 KB
getCnpjInfo 1.8 KB 1.8 KB 0 B 1010 B
getCpfInfo 1.7 KB 1.7 KB 0 B 999 B
getFormatLicensePlate 1.1 KB 1.1 KB 0 B 691 B
getHolidays 6.1 KB 6.1 KB 0 B 2.6 KB
getIbanInfo 1.6 KB 1.6 KB 0 B 955 B
getLegalNature 6.3 KB 6.3 KB 0 B 2.3 KB
getLegalNatures 5.9 KB 5.9 KB 0 B 2.1 KB
getLegalNaturesByCategory 6.5 KB 6.5 KB 0 B 2.4 KB
getMunicipalities 156.4 KB 156.4 KB 0 B 50.3 KB
getMunicipality 154.9 KB 154.9 KB 0 B 50.3 KB
🆕 getMunicipalityByCep 393.3 KB new 98.8 KB
getMunicipalityByCode 156.5 KB 156.5 KB 0 B 50.4 KB
getNfeKeyInfo 2.7 KB 2.7 KB 0 B 1.5 KB
getPixKeyInfo 4.5 KB 4.5 KB 0 B 2.0 KB
getPixPayloadInfo 2.9 KB 2.9 KB 0 B 1.4 KB
🆕 getStateByCep 4.5 KB new 1.5 KB
getStateByIbgeCode 3.2 KB 3.2 KB 0 B 1.1 KB
getStateCodeByName 3.2 KB 3.2 KB 0 B 1.1 KB
getStateNameByCode 3.1 KB 3.1 KB 0 B 1.0 KB
getStates 3.0 KB 3.0 KB 0 B 1019 B
getTimezoneByState 1.6 KB 1.6 KB 0 B 809 B
isBusinessDay 6.5 KB 6.5 KB 0 B 2.7 KB
isHoliday 6.4 KB 6.4 KB 0 B 2.7 KB
isValidBankAccount 7.4 KB 7.4 KB 0 B 2.8 KB
isValidBoleto 2.4 KB 2.4 KB 0 B 1.3 KB
isValidCEP 984 B 984 B 0 B 610 B
isValidCNPJ 1.6 KB 1.6 KB 0 B 912 B
isValidCPF 1.3 KB 1.3 KB 0 B 805 B
isValidCaepf 1.5 KB 1.5 KB 0 B 912 B
isValidCbo 119.2 KB 119.2 KB 0 B 30.7 KB
isValidCei 1.5 KB 1.5 KB 0 B 899 B
isValidCep 984 B 984 B 0 B 610 B
isValidCertidao 1.6 KB 1.6 KB 0 B 938 B
isValidCfop 68.9 KB 68.9 KB 0 B 6.9 KB
isValidCnae 94.0 KB 94.0 KB 0 B 21.2 KB
isValidCnh 1.4 KB 1.4 KB 0 B 856 B
isValidCno 1.5 KB 1.5 KB 0 B 901 B
isValidCnpj 1.6 KB 1.6 KB 0 B 912 B
isValidCns 1.5 KB 1.5 KB 0 B 925 B
isValidCpf 1.3 KB 1.3 KB 0 B 805 B
isValidCreditCard 1.4 KB 1.4 KB 0 B 868 B
isValidCsosn 1.2 KB 1.2 KB 0 B 737 B
isValidCst 1.8 KB 1.8 KB 0 B 1.0 KB
isValidEmail 1.0 KB 1.0 KB 0 B 622 B
isValidIE 5.7 KB 5.7 KB 0 B 2.1 KB
isValidIban 1.3 KB 1.3 KB 0 B 836 B
isValidIe 5.7 KB 5.7 KB 0 B 2.1 KB
isValidLandlinePhone 1.5 KB 1.5 KB 0 B 932 B
isValidLegalNature 5.8 KB 5.8 KB 0 B 2.1 KB
isValidLicensePlate 1.1 KB 1.1 KB 0 B 702 B
isValidMobilePhone 1.6 KB 1.6 KB 0 B 971 B
isValidNcm 114.2 KB 114.2 KB 0 B 24.6 KB
isValidNfeKey 2.7 KB 2.7 KB 0 B 1.5 KB
isValidPIS 1.2 KB 1.2 KB 0 B 785 B
isValidPassport 1.0 KB 1.0 KB 0 B 654 B
isValidPhone 2.6 KB 2.6 KB 0 B 1.3 KB
isValidPis 1.2 KB 1.2 KB 0 B 785 B
isValidPixKey 4.6 KB 4.6 KB 0 B 2.1 KB
isValidPixPayload 2.9 KB 2.9 KB 0 B 1.5 KB
isValidProcessoJuridico 1.3 KB 1.3 KB 0 B 787 B
isValidRegistroProfissional 1.6 KB 1.6 KB 0 B 964 B
isValidRenavam 1.3 KB 1.3 KB 0 B 815 B
isValidServicePhone 1.5 KB 1.5 KB 0 B 845 B
isValidSuframa 1.4 KB 1.4 KB 0 B 883 B
isValidVin 1.6 KB 1.6 KB 0 B 995 B
isValidVoterId 1.6 KB 1.6 KB 0 B 900 B
parseBoleto 1020 B 1020 B 0 B 634 B
parseCaepf 1003 B 1003 B 0 B 621 B
parseCbo 1002 B 1002 B 0 B 620 B
parseCei 1003 B 1003 B 0 B 619 B
parseCep 1002 B 1002 B 0 B 620 B
parseCertidao 1003 B 1003 B 0 B 620 B
parseCfop 1002 B 1002 B 0 B 620 B
parseCnae 1002 B 1002 B 0 B 620 B
parseCnh 1003 B 1003 B 0 B 621 B
parseCno 1003 B 1003 B 0 B 619 B
parseCnpj 1.1 KB 1.1 KB 0 B 668 B
parseCns 1003 B 1003 B 0 B 621 B
parseCpf 1003 B 1003 B 0 B 621 B
parseCurrency 1.4 KB 1.4 KB 0 B 881 B
parseIban 1.0 KB 1.0 KB 0 B 638 B
parseLegalNature 1002 B 1002 B 0 B 620 B
parseLicensePlate 1.0 KB 1.0 KB 0 B 638 B
parseNcm 1002 B 1002 B 0 B 620 B
parseNfeKey 1.0 KB 1.0 KB 0 B 658 B
parsePassport 1.0 KB 1.0 KB 0 B 637 B
parsePhone 1.1 KB 1.1 KB 0 B 707 B
parsePis 1003 B 1003 B 0 B 621 B
parseProcessoJuridico 1003 B 1003 B 0 B 621 B
parseSuframa 1002 B 1002 B 0 B 620 B
parseVoterId 1.0 KB 1.0 KB 0 B 649 B
removeAccents 953 B 953 B 0 B 593 B
subBusinessDays 6.9 KB 6.9 KB 0 B 2.9 KB
toStandardSchema 1.1 KB 1.1 KB 0 B 714 B
How this is measured

Every 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 tree-shaking: accepted label.

@codecov

codecov Bot commented Sep 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (214ce29) to head (a42c871).

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     
Flag Coverage Δ
node 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@hyanmandian

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…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.
@hyanmandian
hyanmandian force-pushed the claude/get-state-by-cep branch from 8c13c81 to 2b00546 Compare September 22, 2026 05:01
@vercel

vercel Bot commented Sep 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
brazilian-utils Ready Ready Preview Sep 22, 2026 1:10pm UTC

@hyanmandian
hyanmandian changed the base branch from main to claude/get-cnpj-info September 22, 2026 05:01
@pkg-pr-new

pkg-pr-new Bot commented Sep 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@brazilian-utils/brazilian-utils@562

commit: a42c871

@hyanmandian

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 22, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…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.
@hyanmandian hyanmandian changed the title feat(get-state-by-cep): add getStateByCep, the state that owns a CEP range feat: add getStateByCep and getMunicipalityByCep, the state and municipality that own a CEP range Sep 22, 2026
@hyanmandian

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 22, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Pull request base or head changed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

This branch was successfully deployed

1 active deployment
Preview a42c8715 Deployed Sep 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant