From 538f915af3d260c9819661cfad008bae0b943640 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 16:17:03 +0000 Subject: [PATCH 1/4] feat(area-codes): add getMunicipalitiesByAreaCode and getAreaCodeByMunicipalityCode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There was no way to go from a DDD to the municipalities that dial it, or from a municipality to its DDD; getAreaCodeInfo only knew the state. Both now come from the Código Nacional in force for every municipality, in the Codigos_Nacionais.csv Anatel publishes in pgcn.zip (21/09/2026): 5,571 rows in force, the same municipalities as the IBGE table, and the same 67 DDDs and four cross-border ones as AREA_CODE_STATES. getAreaCodeByMunicipalityCode builds on getMunicipalityByCode and returns the DDD, or null for a code that is not a municipality. getMunicipalitiesByAreaCode builds on getAreaCodeInfo and lists the municipalities of the seat state first, then those of the state the DDD crosses into (61 covers 12 municipalities of Goiás; 42, 47 and 49 one each). scripts/area-codes.ts generates the table and runs with the other datasets. It stores the two digits of each municipality's DDD in ascending order of the municipality code, one string per state, so it adds 11.5 KB minified (2.6 KB gzipped) on top of the municipality table the two functions already need. A test checks the table covers exactly the municipalities of the IBGE table and agrees with AREA_CODE_STATES. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH --- context7.json | 2 +- docs/getting-started.md | 1 + docs/pt-br/getting-started.md | 1 + docs/pt-br/utilities.md | 38 ++++ docs/utilities.md | 38 ++++ jsr.json | 2 + scripts/area-codes.ts | 178 ++++++++++++++++++ scripts/data-summary.ts | 2 + scripts/data.ts | 2 + scripts/read-xlsx-sheet.ts | 10 + .../constants/municipality-area-codes.test.ts | 58 ++++++ .../constants/municipality-area-codes.ts | 46 +++++ .../read-municipality-area-code.test.ts | 23 +++ .../read-municipality-area-code.ts | 35 ++++ ...get-area-code-by-municipality-code.test.ts | 77 ++++++++ .../get-area-code-by-municipality-code.ts | 39 ++++ .../get-municipalities-by-area-code.test.ts | 112 +++++++++++ .../get-municipalities-by-area-code.ts | 45 +++++ src/index.test.ts | 2 + src/index.ts | 2 + 20 files changed, 712 insertions(+), 1 deletion(-) create mode 100644 scripts/area-codes.ts create mode 100644 src/_internals/constants/municipality-area-codes.test.ts create mode 100644 src/_internals/constants/municipality-area-codes.ts create mode 100644 src/_internals/read-municipality-area-code/read-municipality-area-code.test.ts create mode 100644 src/_internals/read-municipality-area-code/read-municipality-area-code.ts create mode 100644 src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.test.ts create mode 100644 src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.ts create mode 100644 src/get-municipalities-by-area-code/get-municipalities-by-area-code.test.ts create mode 100644 src/get-municipalities-by-area-code/get-municipalities-by-area-code.ts diff --git a/context7.json b/context7.json index 56e1d8b3c..cda228ff9 100644 --- a/context7.json +++ b/context7.json @@ -30,7 +30,7 @@ "rules": [ "The package has zero runtime dependencies and ships as ESM plus a UMD build; nothing else needs to be installed to use it.", "Import from the root: import { isValidCpf } from '@brazilian-utils/brazilian-utils'. Every util is also a kebab-case subpath, e.g. '@brazilian-utils/brazilian-utils/is-valid-cpf'.", - "Use the subpaths to lazy-load the dataset-backed utils (getMunicipalities, getMunicipalityByCode, getCnae, getCbo, getCest, getCfop, getCid10, isValidCid10, getClassTrib, isValidNcm, getBanks, getNbs, getServiceItem): each one embeds a large official table.", + "Use the subpaths to lazy-load the dataset-backed utils (getMunicipalities, getMunicipalityByCode, getMunicipalitiesByAreaCode, getAreaCodeByMunicipalityCode, getCnae, getCbo, getCest, getCfop, getCid10, isValidCid10, getClassTrib, isValidNcm, getBanks, getNbs, getServiceItem): each one embeds a large official table.", "Never import the same util from both the root and its subpath in one app: a bundler treats them as two unrelated modules and bundles the dataset twice.", "Public functions never throw on bad input (null, undefined, wrong type): isValid* return false, format* and parse* return '', single-item getters return null, list getters return [].", "The only utils that reject are the async getAddressInfoByCep (GetAddressInfoByCepError: NotFound, Validation, Service) and getCepInfoByAddress (GetCepInfoByAddressError: NotFound, Validation).", diff --git a/docs/getting-started.md b/docs/getting-started.md index 77e203702..5a69c6ab4 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -69,6 +69,7 @@ A few utils embed an official dataset and weigh far more than everything else co | Util | Dataset | Minified | Gzipped | | --- | --- | --- | --- | | `getCid10` | CID-10 V2008 categories and subcategories plus the SIM `U07` codes, with the DATASUS descriptions | 988.3 KB | 123.6 KB | +| `getMunicipalitiesByAreaCode` · `getAreaCodeByMunicipalityCode` | 5571 IBGE municipalities, with the DDD of each (Anatel) | 165.0 - 167.8 KB | 52.1 - 52.8 KB | | `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 IBGE municipalities, with names and codes | 153.6 - 154.0 KB | 49.4 - 49.7 KB | | `getCities` | 5571 IBGE municipality names | 153.4 KB | 49.2 KB | | `getCbo` | CBO 2002 occupation titles | 115.7 KB | 29.5 KB | diff --git a/docs/pt-br/getting-started.md b/docs/pt-br/getting-started.md index 74adb6555..bcc5ae1f6 100644 --- a/docs/pt-br/getting-started.md +++ b/docs/pt-br/getting-started.md @@ -69,6 +69,7 @@ Alguns utilitários embutem uma base de dados oficial e pesam muito mais que tod | Utilitário | Base de dados | Minificado | Gzip | | --- | --- | --- | --- | | `getCid10` | categorias e subcategorias da CID-10 V2008 mais os códigos `U07` do SIM, com as descrições do DATASUS | 988,3 KB | 123,6 KB | +| `getMunicipalitiesByAreaCode` · `getAreaCodeByMunicipalityCode` | 5571 municípios do IBGE, com o DDD de cada um (Anatel) | 165,0 - 167,8 KB | 52,1 - 52,8 KB | | `getMunicipalities` · `getMunicipalityByCode` · `getMunicipality` | 5571 municípios do IBGE, com nomes e códigos | 153,6 - 154,0 KB | 49,4 - 49,7 KB | | `getCities` | nomes dos 5571 municípios do IBGE | 153,4 KB | 49,2 KB | | `getCbo` | títulos das ocupações da CBO 2002 | 115,7 KB | 29,5 KB | diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index ee80c1e91..4b1f8cda1 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -972,6 +972,25 @@ isValidServicePhone('11987654321'); // false (número geográfico) Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [página de SUP da Anatel](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais/servicos-de-utilidade-publica-e-de-emergencia), [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151), [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86). +### getAreaCodeByMunicipalityCode + +Retorna o DDD (código de área) de um município brasileiro a partir do código IBGE de 7 dígitos, segundo a tabela da Anatel dos Códigos Nacionais em vigor. + +- Aceita o código como o `getMunicipalityByCode`: string (ignorando espaços e hífens) ou número inteiro não negativo. +- Retorna o DDD como número, ou `null` quando o código não é de um município. Cada um dos 5.571 municípios tem exatamente um DDD. +- O DDD quase sempre segue a divisa dos estados. As exceções: o 61 também cobre 12 municípios de Goiás no entorno de Brasília, e Porto União (SC) usa o 42, Rio Negro (PR) o 47 e Barracão (PR) o 49. + +```javascript +import { getAreaCodeByMunicipalityCode } from '@brazilian-utils/brazilian-utils'; + +getAreaCodeByMunicipalityCode('3550308'); // 11 (São Paulo/SP) +getAreaCodeByMunicipalityCode(3304557); // 21 (Rio de Janeiro/RJ) +getAreaCodeByMunicipalityCode('4122305'); // 47 (Rio Negro/PR) +getAreaCodeByMunicipalityCode('0000000'); // null +``` + +Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Códigos Nacionais da Anatel](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais), [tabela da Anatel dos Códigos Nacionais por município (21/09/2026)](https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais). + ### getAreaCodeInfo Retorna o estado e a região a que um DDD brasileiro (código de área) pertence, dentre os 67 DDDs em uso no Plano Geral de Numeração da Anatel. Aceita string ou número inteiro não negativo. @@ -1020,6 +1039,25 @@ getAreaCodesByState('XX'); // [] Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Códigos Nacionais da Anatel](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais), [tabela da Anatel dos Códigos Nacionais por município (21/09/2026)](https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais). +### getMunicipalitiesByAreaCode + +Lista os municípios brasileiros que usam um DDD (código de área), segundo a tabela da Anatel dos Códigos Nacionais em vigor. + +- Aceita o DDD como o `getAreaCodeInfo`: string (com parênteses, espaços e hífens) ou número inteiro não negativo. +- Retorna um array de `{ code, name, stateCode }` (`Municipality`): primeiro os municípios do estado sede, depois os do outro estado em que o DDD entra, cada estado em ordem alfabética. Retorna `[]` quando o DDD não está em uso. + +```javascript +import { getMunicipalitiesByAreaCode } from '@brazilian-utils/brazilian-utils'; + +getMunicipalitiesByAreaCode(68).length; // 22 (todos os municípios do Acre) +getMunicipalitiesByAreaCode('(61)').length; // 13 (Brasília e 12 municípios de Goiás) +getMunicipalitiesByAreaCode('47').at(-1); // { code: '4122305', name: 'Rio Negro', stateCode: 'PR' } +getMunicipalitiesByAreaCode('20'); // [] +``` + +Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Códigos Nacionais da Anatel](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais), [tabela da Anatel dos Códigos Nacionais por município (21/09/2026)](https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais). + + ## Placa de veículo ### isValidLicensePlate diff --git a/docs/utilities.md b/docs/utilities.md index 4805e3fe8..1bfde00d6 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -972,6 +972,25 @@ isValidServicePhone('11987654321'); // false (geographic number) Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Anatel SUP page](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais/servicos-de-utilidade-publica-e-de-emergencia), [Ato Anatel nº 43.151/2004](https://informacoes.anatel.gov.br/legislacao/atos-de-numeracao/2004/1648-ato-43151), [Resolução nº 86/1998](https://informacoes.anatel.gov.br/legislacao/resolucoes/1998/336-resolucao-86). +### getAreaCodeByMunicipalityCode + +Get the DDD (area code) a Brazilian municipality dials, given its 7-digit IBGE code, from the Anatel table of the Códigos Nacionais in force. + +- Accepts the code the way `getMunicipalityByCode` does: a string (spaces and hyphens ignored) or a non-negative integer. +- Returns the DDD as a number, or `null` when the code is not a municipality. Every one of the 5,571 municipalities has exactly one DDD. +- A DDD mostly follows state lines. The exceptions: 61 also covers 12 municipalities of Goiás around Brasília, and Porto União (SC) dials 42, Rio Negro (PR) 47 and Barracão (PR) 49. + +```javascript +import { getAreaCodeByMunicipalityCode } from '@brazilian-utils/brazilian-utils'; + +getAreaCodeByMunicipalityCode('3550308'); // 11 (São Paulo/SP) +getAreaCodeByMunicipalityCode(3304557); // 21 (Rio de Janeiro/RJ) +getAreaCodeByMunicipalityCode('4122305'); // 47 (Rio Negro/PR) +getAreaCodeByMunicipalityCode('0000000'); // null +``` + +Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Anatel Códigos Nacionais](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais), [Anatel table of the Códigos Nacionais by municipality (21/09/2026)](https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais). + ### getAreaCodeInfo Get the state and region a Brazilian DDD (area code) belongs to, out of the 67 DDDs in use under the Anatel Plano Geral de Numeração. Accepts a string or a non-negative integer. @@ -1020,6 +1039,25 @@ getAreaCodesByState('XX'); // [] Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Anatel Códigos Nacionais](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais), [Anatel table of the Códigos Nacionais by municipality (21/09/2026)](https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais). +### getMunicipalitiesByAreaCode + +List the Brazilian municipalities that dial a given DDD (area code), from the Anatel table of the Códigos Nacionais in force. + +- Accepts the DDD the way `getAreaCodeInfo` does: a string (parentheses, spaces and hyphens allowed) or a non-negative integer. +- Returns an array of `{ code, name, stateCode }` (`Municipality`): the seat state's municipalities first, then those of the other state the DDD crosses into, each state's sorted by name. Returns `[]` when the DDD is not in use. + +```javascript +import { getMunicipalitiesByAreaCode } from '@brazilian-utils/brazilian-utils'; + +getMunicipalitiesByAreaCode(68).length; // 22 (every municipality of Acre) +getMunicipalitiesByAreaCode('(61)').length; // 13 (Brasília and 12 municipalities of Goiás) +getMunicipalitiesByAreaCode('47').at(-1); // { code: '4122305', name: 'Rio Negro', stateCode: 'PR' } +getMunicipalitiesByAreaCode('20'); // [] +``` + +Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legislacao/resolucoes/2022/1641-resolucao-749), [Anatel Códigos Nacionais](https://www.gov.br/anatel/pt-br/regulado/numeracao/codigos-nacionais), [Anatel table of the Códigos Nacionais by municipality (21/09/2026)](https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais). + + ## License plate ### isValidLicensePlate diff --git a/jsr.json b/jsr.json index 5842acbf3..c9dfa329c 100644 --- a/jsr.json +++ b/jsr.json @@ -53,6 +53,7 @@ "./generate-suframa": "./src/generate-suframa/generate-suframa.ts", "./generate-voter-id": "./src/generate-voter-id/generate-voter-id.ts", "./get-address-info-by-cep": "./src/get-address-info-by-cep/get-address-info-by-cep.ts", + "./get-area-code-by-municipality-code": "./src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.ts", "./get-area-code-info": "./src/get-area-code-info/get-area-code-info.ts", "./get-area-codes-by-state": "./src/get-area-codes-by-state/get-area-codes-by-state.ts", "./get-bank-by-code": "./src/get-bank-by-code/get-bank-by-code.ts", @@ -79,6 +80,7 @@ "./get-legal-natures": "./src/get-legal-natures/get-legal-natures.ts", "./get-legal-natures-by-category": "./src/get-legal-natures-by-category/get-legal-natures-by-category.ts", "./get-municipalities": "./src/get-municipalities/get-municipalities.ts", + "./get-municipalities-by-area-code": "./src/get-municipalities-by-area-code/get-municipalities-by-area-code.ts", "./get-municipality": "./src/get-municipality/get-municipality.ts", "./get-municipality-by-code": "./src/get-municipality-by-code/get-municipality-by-code.ts", "./get-nbs": "./src/get-nbs/get-nbs.ts", diff --git a/scripts/area-codes.ts b/scripts/area-codes.ts new file mode 100644 index 000000000..fbddf1a7b --- /dev/null +++ b/scripts/area-codes.ts @@ -0,0 +1,178 @@ +#!/usr/bin/env node + +import { DATA as STATES } from "../src/_internals/constants/states.ts"; +import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts"; +import { runAsEntryPoint, writeGeneratedFiles } from "./lookup-table.ts"; +import { readZipFile } from "./read-xlsx-sheet.ts"; + +const PGCN_URL = "https://www.anatel.gov.br/dadosabertos/paineis_de_dados/areastarifarias/pgcn.zip"; + +const PGCN_CSV = "Codigos_Nacionais.csv"; + +const PGCN_HEADER = [ + "CO_MUNICIPIO", + "SG_UF", + "NO_UF", + "NO_MUNICIPIO", + "CN", + "DT_INICIO_VIGENCIA", + "DT_FIM_VIGENCIA", + "DE_ALTERACAO_REGULAMENTAR", + "VIGENTE", +]; + +const MUNICIPALITY_CODE_REGEX = /^\d{7}$/; + +const AREA_CODE_REGEX = /^[1-9][1-9]$/; + +const STATE_CODES = new Set(STATES.map((state) => state.code)); + +/** + * Splits one `;` separated row of the Anatel CSV, whose fields may be quoted (`"Alta Floresta + * D'Oeste"`), a quote inside a quoted field being written twice. + * @param {string} row - The row. + * @returns {string[]} The fields of the row. + */ +const splitRow = (row: string): string[] => { + const fields: string[] = []; + let field = ""; + let quoted = false; + + for (let index = 0; index < row.length; index++) { + const character = row[index]; + + if (quoted) { + if (character === '"' && row[index + 1] === '"') { + field += '"'; + index++; + } else if (character === '"') { + quoted = false; + } else { + field += character; + } + } else if (character === '"') { + quoted = true; + } else if (character === ";") { + fields.push(field); + field = ""; + } else { + field += character; + } + } + + fields.push(field); + + return fields; +}; + +/** + * Reads the Código Nacional (DDD) in force for every municipality of the Anatel + * `Codigos_Nacionais.csv`. A municipality's earlier codes stay in the file with `VIGENTE` set to + * `Não`, so only the rows in force are kept, and a municipality with more than one of them is an + * error. + * @param {string} csv - The decoded CSV. + * @returns {Record>} The DDD of every municipality, by state and + * by 7 digit IBGE municipality code. + */ +export const parseAreaCodes = (csv: string): Record> => { + const [header, ...rows] = csv.replace(/^/, "").split(/\r?\n/); + + if (header === undefined || splitRow(header).join(";") !== PGCN_HEADER.join(";")) { + throw new Error(`${PGCN_CSV} header is not "${PGCN_HEADER.join(";")}"`); + } + + const areaCodes: Record> = {}; + + for (const row of rows) { + if (row.trim() === "") continue; + + const fields = splitRow(row); + const code = fields[0] ?? ""; + const stateCode = fields[1] ?? ""; + const areaCode = fields[4] ?? ""; + const inForce = fields[8] ?? ""; + + if (inForce !== "Sim") continue; + + if ( + !MUNICIPALITY_CODE_REGEX.test(code) || + !STATE_CODES.has(stateCode) || + !AREA_CODE_REGEX.test(areaCode) + ) { + throw new Error(`${PGCN_CSV} has an unexpected row: ${row}`); + } + + const state = areaCodes[stateCode] ?? {}; + + areaCodes[stateCode] = state; + + if (Object.hasOwn(state, code)) { + throw new Error(`${PGCN_CSV} has two codes in force for the municipality ${code}`); + } + + state[code] = Number(areaCode); + } + + return areaCodes; +}; + +/** + * Renders the module the DDD of every municipality is shipped as: for each state, one string + * holding the two digits of each municipality's DDD, in ascending order of the municipality code. + * @param {Record>} areaCodes - What `parseAreaCodes` returns. + * @returns {Record} The content of the generated file, by its path. + */ +export const renderAreaCodes = ( + areaCodes: Record>, +): Record => { + const entries = [...STATE_CODES].sort().map((stateCode) => { + const state = areaCodes[stateCode]; + + if (state === undefined) throw new Error(`${PGCN_CSV} has no municipality of ${stateCode}`); + + const digits = Object.keys(state) + .sort() + .map((code) => String(state[code])) + .join(""); + + return `\t${stateCode}: "${digits}",`; + }); + + return { + "./src/_internals/constants/municipality-area-codes.ts": `import { type StateCode } from "./states"; + +/** + * The DDD (Código Nacional) in force for every Brazilian municipality. For each state, the two + * digits of each municipality's DDD follow one another in ascending order of the 7 digit IBGE + * municipality code, so the municipality with the n-th smallest code of the state has its DDD at + * characters \`2n\` and \`2n + 1\`. The municipalities are the same 5,571 as \`DATA\` in + * \`municipalities.ts\`, which a test checks. + * + * Generated by \`node ./scripts/area-codes.ts\` from the \`Codigos_Nacionais.csv\` Anatel + * publishes, keeping only the rows in force (\`VIGENTE\` is \`Sim\`). Do not edit by hand. + * + * @see Official: https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais + * Anatel, Painel de Dados de Áreas Tarifárias, "Códigos Nacionais". + * @see Official: ${PGCN_URL} + * \`Codigos_Nacionais.csv\`, the Código Nacional of every municipality. + */ +export const MUNICIPALITY_AREA_CODES: Record = { +${entries.join("\n")} +}; +`, + }; +}; + +const main = async (): Promise => { + const response = await fetchWithRetry(PGCN_URL); + + if (!response.ok) throw new Error(`Anatel PGCN request failed with status ${response.status}`); + + const archive = Buffer.from(await response.arrayBuffer()); + + const areaCodes = parseAreaCodes(readZipFile(archive, PGCN_CSV)); + + await writeGeneratedFiles(renderAreaCodes(areaCodes)); +}; + +await runAsEntryPoint(import.meta.filename, main); diff --git a/scripts/data-summary.ts b/scripts/data-summary.ts index 8e8ce2043..160c93342 100644 --- a/scripts/data-summary.ts +++ b/scripts/data-summary.ts @@ -33,6 +33,8 @@ const DATASETS: Record = { "src/_internals/constants/ibs-cbs.ts": "CST-IBS/CBS and cClassTrib (Portal Nacional da NF-e, Informe Técnico 2025.002)", "src/_internals/constants/municipalities.ts": "Municipalities (IBGE)", + "src/_internals/constants/municipality-area-codes.ts": + "DDD of every municipality (Anatel, Códigos Nacionais)", "src/_internals/constants/nbs-descriptions.ts": "NBS 2.0 descriptions (MDIC)", "src/_internals/constants/nbs.ts": "NBS 2.0 codes (MDIC)", "src/_internals/constants/service-item-descriptions.ts": diff --git a/scripts/data.ts b/scripts/data.ts index 66e3005d1..f2427ffda 100644 --- a/scripts/data.ts +++ b/scripts/data.ts @@ -20,6 +20,7 @@ const run = (command: string, args: string[]): Promise => }); const generators = [ + "area-codes.ts", "banks.ts", "cbo.ts", "cest.ts", @@ -36,6 +37,7 @@ const generators = [ ]; const generatedFiles = [ + "./src/_internals/constants/municipality-area-codes.ts", "./src/_internals/constants/banks.ts", "./src/_internals/constants/cbo-descriptions.ts", "./src/_internals/constants/cbo.ts", diff --git a/scripts/read-xlsx-sheet.ts b/scripts/read-xlsx-sheet.ts index c328f0c3b..7cc84cde2 100644 --- a/scripts/read-xlsx-sheet.ts +++ b/scripts/read-xlsx-sheet.ts @@ -323,3 +323,13 @@ export const readXlsxSheets = (workbook: Buffer): Map => { return new Map(opened.sheets.map((sheet) => [sheet.name, readSheet(opened, sheet)])); }; + +/** + * Reads one file of a zip archive as UTF-8, for a dataset published as a zipped CSV rather than + * as a workbook. + * @param {Buffer} archive - The `.zip` file. + * @param {string} path - The path of the file inside the archive. + * @returns {string} The content of the file. + */ +export const readZipFile = (archive: Buffer, path: string): string => + readFile(unzip(archive), path); diff --git a/src/_internals/constants/municipality-area-codes.test.ts b/src/_internals/constants/municipality-area-codes.test.ts new file mode 100644 index 000000000..c0271c5d2 --- /dev/null +++ b/src/_internals/constants/municipality-area-codes.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from "../test/runtime"; +import { AREA_CODE_SECONDARY_STATES, AREA_CODE_STATES, VALID_AREA_CODES } from "./area-codes"; +import { DATA as CITIES_DATA } from "./municipalities"; +import { MUNICIPALITY_AREA_CODES } from "./municipality-area-codes"; +import { type StateCode } from "./states"; + +const STATE_CODES = Object.keys(CITIES_DATA) as StateCode[]; + +const areaCodesOf = (stateCode: StateCode): number[] => + (MUNICIPALITY_AREA_CODES[stateCode].match(/\d{2}/g) ?? []).map(Number); + +describe("MUNICIPALITY_AREA_CODES", () => { + test("should cover the same states as the municipalities table", () => { + expect(Object.keys(MUNICIPALITY_AREA_CODES).sort()).toEqual([...STATE_CODES].sort()); + }); + + test("should hold two digits for every municipality of each state", () => { + for (const stateCode of STATE_CODES) { + expect(MUNICIPALITY_AREA_CODES[stateCode]).toMatch(/^(?:\d{2})+$/); + expect(MUNICIPALITY_AREA_CODES[stateCode].length).toBe(CITIES_DATA[stateCode].length * 2); + } + }); + + test("should only hold DDDs in use, each listing the state of the municipality", () => { + for (const stateCode of STATE_CODES) { + for (const areaCode of areaCodesOf(stateCode)) { + expect(VALID_AREA_CODES).toContain(areaCode); + expect([ + AREA_CODE_STATES[areaCode], + ...(AREA_CODE_SECONDARY_STATES[areaCode] ?? []), + ]).toContain(stateCode); + } + } + }); + + test("should give every one of the 67 DDDs, and every state a DDD lists, a municipality", () => { + const statesByAreaCode = new Map>(); + + for (const stateCode of STATE_CODES) { + for (const areaCode of areaCodesOf(stateCode)) { + const states = statesByAreaCode.get(areaCode) ?? new Set(); + + states.add(stateCode); + statesByAreaCode.set(areaCode, states); + } + } + + expect([...statesByAreaCode.keys()].sort((a, b) => a - b)).toEqual( + [...VALID_AREA_CODES].sort((a, b) => a - b), + ); + + for (const [areaCode, states] of statesByAreaCode) { + expect([...states].sort()).toEqual( + [AREA_CODE_STATES[areaCode], ...(AREA_CODE_SECONDARY_STATES[areaCode] ?? [])].sort(), + ); + } + }); +}); diff --git a/src/_internals/constants/municipality-area-codes.ts b/src/_internals/constants/municipality-area-codes.ts new file mode 100644 index 000000000..558faa633 --- /dev/null +++ b/src/_internals/constants/municipality-area-codes.ts @@ -0,0 +1,46 @@ +import { type StateCode } from "./states"; + +/** + * The DDD (Código Nacional) in force for every Brazilian municipality. For each state, the two + * digits of each municipality's DDD follow one another in ascending order of the 7 digit IBGE + * municipality code, so the municipality with the n-th smallest code of the state has its DDD at + * characters `2n` and `2n + 1`. The municipalities are the same 5,571 as `DATA` in + * `municipalities.ts`, which a test checks. + * + * Generated by `node ./scripts/area-codes.ts` from the `Codigos_Nacionais.csv` Anatel + * publishes, keeping only the rows in force (`VIGENTE` is `Sim`). Do not edit by hand. + * + * @see Official: https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais + * Anatel, Painel de Dados de Áreas Tarifárias, "Códigos Nacionais". + * @see Official: https://www.anatel.gov.br/dadosabertos/paineis_de_dados/areastarifarias/pgcn.zip + * `Codigos_Nacionais.csv`, the Código Nacional of every municipality. + */ +export const MUNICIPALITY_AREA_CODES: Record = { + AC: "68686868686868686868686868686868686868686868", + AL: "828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282828282", + AM: "9797979797979297929797929792929797929297979797979797979292979297979797929292979792929292979297929297979797929297979797979292", + AP: "96969696969696969696969696969696", + BA: "777575757577737573737575747775747775757574757377757575737573777475747777747377747573737775777577777575777775777777737777757577747777747574737173747477747375717777757575747773757774757771777475757573777575757575777775777775777773757775747371777575777575757375777574737377737574757375777773757577737773747775737373777477757773737375757375777377757574737573737573737374737373737775737173737775737373757374737773777777747374757573757573747473747377737775737573757471757777777477777571777774777773777375737573717773747574777577747775737474757775757573757375737375757377757577757477757777757375757575777477747573777473777771747373737477737575747574757775777575777777777575757571757573737375737777757775757575777575777571747573747571757571747577747774777575747177757475777777757574737575737577757475737374747377737575757474747571737775777374", + CE: "88858888888888888888858588858888858888888888888588888585888888888885858588888888858888858888888588888888888888858888858888858888888885888588858888888888888888888888888888858588888888888888888888888888888888858588888888888888888888888888888885888888858885858588888585858885888885888588888888888888888888888588888888888888888588858888888888888888858885858888858588888888", + DF: "61", + ES: "272727282727282827282727272827282728272728282728272727282728272828282728272827282727272728272728272728282727272728272827282727272727282727272728282727272727", + GO: "626264646264616264626262626464626464626464626462626462646464626264646262626264646261646464646464626262626462626262646462626264646162626462646164626464626264646464646262626462616262626264626262626464626262626262626264646264626464626262646262626462646464626464626462646261646262646264626264646262646264626462626264626262646262626262626162646264616464646464646464626264646264616462646462646462626464626462646264626462626262646261626262646462626262646264626262626262646262646462626264616262646162", + MA: "99989898999899989998999898989898999898999898989999999898989998999898989998989899989998989899989899999899989898989899999899989998999998999999999999999899999999999898999998989898999999989999999999989999999999999999999898989898989899989899989898999898999998989898989999999998989998989899989898989999989998989898989899999899999898989898989898999898989999999999989899999998999899999999999999989899999899999999999999989998989898989899989898", + MG: "34373131333334353333333535353235323333353532323331313135333533323132313233343231343437343537353233383233383531373335313232313231323131313333383132323735323837323531333535313838353538383832333531353833383838353135343131323135373535353335353438353534373531343535333231333531343338373133313232333835333135373537343537343535353134353232313133383537333432333338323234383732333433353534333137353538313531313431333531353831353833343133323237353733383335383135313435333338383535343231323831313232333337333533383331313532323337323734333533383331323132383535323734323235353238383338333132313738353133383833333333343338343133383833323533383338343335313835323238353234323435333234383835313535383137373535333538343533313831333134353431333138383133353832333135353533333437353135373133333435343531333535313833383837383133313831333533333138383333333831313238353833343738323434313335313238353733323232343833383537333532333838333333373235313331323135373333333132383138313437333332333335383232383137383538343835343335383538373831353233353333333835353538333131333438353733333832383537323731323135313338333737383232333738373535313135373235383434323233333333313137323538343132323737343533333233313231373338373431353532383237373435353337313833383133353532343534323838343331373131333233323332383831353131313331313234383132323332323234323833313334383331333232323831333833343131313333353231323235333131313532333233353434353231333337383138323531333133383734323233333837313534353835383832333333383833323135313337353331333335313533333837353233333735313532353535353331333135323235383133323135383733343538353831323532333335323833343731333133313234323535323538353334383532383334343338343831383337383538383834383834333131323333353333323235", + MS: "67676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767", + MT: "65666666666665666666666565666565666666666566656665666666666566666666656665666565656566666566666566666566666665666565666666666565666566666565656566666566666665656666666666666666666666656666656666656566666665656666656665666566666666656665656666656665666666666566666666666565666666656666", + PA: "919491919493939391919191919391919491919391949191939494919191919194919191919194919494919391949493949194919191919191939493949391919194919191939191919393919194919394939193919491949194949194939191939391919494949391919191919194919493919191949194919491919491919491919191939191939494919391919394", + PB: "83838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383", + PE: "8187878181878781818187818787818181878187818787818187878181878187818787878181818787878781818181878181818781818187878187818181878781818781818781878787818787878187878781818781818181818781818787878181878187878181818781878181818187878187818787818181878787878781818187818181818181878787878781878781878181818781818787818187878781878781878187818187878181818787878787878181818181", + PI: "8986868989868689868689898989898689898986898686898986898986898986898686898686868986898989898686898986898986898686868686868989898989898989898689868989868986898689898989898989898989898989868689898989898989868689898686868689898686898686898986868689898989868686868689898986868986868689868986898989898689898686898689898689898989898686868986898986898989898989898989898986898986898989868986868986898689898989898689868689898986898989898989868689898989898989", + PR: "434141414444444443444645434441424343434344414343444444414344494346434244464245414643464345444345444343434344424145414141444342424645424345424543414546444446414443434145434646444644444243414344454546444446444445464443414442434446444344444544464242434442434444424344434542414146434543444445424242444442444442444445434446414442434444434343434344434443444344434344444341424243434544444344434342444444414646434544444344444446444642434541424345454443454441444444434544444643424444434546424342444544464242444444414444454642424444444641414346424241424344464241434242444243464443424443414546444441454344464246424243434243424341474344444343464346434344444544444645424344434545434443464444434644434246444443454144424545434443444344464345434343464344444442424444444241454345414444424444424443434245464441424644", + RJ: "2422222422222424212222222122222422222222222222222124212221212222222421222222212121242124222221212221212424242424242224242122242124242221222222222122212224222224222122222121222424222424", + RN: "8484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484848484", + RO: "69696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969", + RR: "959595959595959595959595959595", + RS: "5354555555555554555451515155545154515154515153515153515554535151545154555551545454545455555555545451555151555555515155555151545555515454545554515554515553545351515451555351515154515454545455545453515551555451545553515154515455545154555554545455515555545154555555515554515153555551515151545554545454535455545554545151515155545455545151545454555155555455545154545551545451515451545551515553555154545454515551515155555454555555515551545551555554535554555555515154545155555551515455515551515454515451555451555451555454545154515351515154545454545454545451555551545554545554515151555554555154515555555154555151515154545153535553545554555553545553555154545151555555555151515451555455545551555453515151555551545455555555555551545554515155515553555554535555545155545555545554555455515455545554515553545154515355545555555555515555555155545455515551515455555154555154515451555455515154515454515151555155515154555551515154515155515455545155515455515553555455555451515154515154515451555454545455555455555151", + SC: "49494747494949484849494849484847494748484949474747484747484949474749474847494749494949474847494749494947494749494948474948494949494749484949494947494849494949494747494849494948494849494948474748484847494749494949494748474848474749494949494949494749474747494747494848474949474749494948494748494849474749494748474849494749484749494947484847494948484948494949494948494949474948494848474948474749494949474949494742474849474749494849474747474847494947494749494849494848474949484947484949494749484748494849494948484949484947494948484947494948484749474848494748494849494948494949474947474949494948", + SE: "797979797979797979797979797979797979797979797979797979797979797979797979797979797979797979797979797979797979797979797979797979797979797979797979797979", + SP: "181719191914191415181716181117181414191617191918151418121715111815161412161914141214171911171811171418141714171215181414151517161114161417181413181811161414151115181416141411181816141518161411121219141811181113171615191112141312181614151519121117191618171717141515191817171914191814191917111616181213121911171916141716181416181319171711111819191417181814171614111817181816111118141417161714181414171715161817171418111215161311161714191914141416171416151718141611131213181918171814151917151619171815141714131511151511191516151418111513111419161911111617161217131917121116111416111817141511181311121518121914191914121819181416181418141717171111141818171814171816111717171614131718181717191119191917131916171718121916111618181812111717171715171716171818171719171614141717141716111418141718181717171818181217141718141317161819141714111716191813181814131815151217111812111914141711181914161818111714141617171715151912171816131414181818181415181412141918121814131615161516141815111615161618191911171812181714181716111819111514181717191217191619141617191116191817111719161718111619181912181813121111161719171718161612191712111214151119141112191613151417161619161317121915181911181716111814161619171519161614151818151214181715141514161217111418181717121417171714171719181119111114191716171517181419", + TO: "63636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363", +}; diff --git a/src/_internals/read-municipality-area-code/read-municipality-area-code.test.ts b/src/_internals/read-municipality-area-code/read-municipality-area-code.test.ts new file mode 100644 index 000000000..0956fbd8e --- /dev/null +++ b/src/_internals/read-municipality-area-code/read-municipality-area-code.test.ts @@ -0,0 +1,23 @@ +import { DATA as CITIES_DATA } from "../constants/municipalities"; +import { describe, expect, test } from "../test/runtime"; +import { readMunicipalityAreaCode } from "./read-municipality-area-code"; + +describe("readMunicipalityAreaCode", () => { + test("should read the DDD of a municipality", () => { + expect(readMunicipalityAreaCode("SP", "3550308")).toBe(11); + expect(readMunicipalityAreaCode("SP", "3509502")).toBe(19); + expect(readMunicipalityAreaCode("SC", "4213609")).toBe(42); + }); + + test("should read the first and the last municipality of a state, by code", () => { + const codes = CITIES_DATA.AM.map(([, code]) => code).sort(); + + expect(readMunicipalityAreaCode("AM", codes[0] ?? "")).toBe(97); + expect(readMunicipalityAreaCode("AM", codes.at(-1) ?? "")).toBe(92); + }); + + test("should give the same answer on a second lookup of the state", () => { + expect(readMunicipalityAreaCode("AC", "1200401")).toBe(68); + expect(readMunicipalityAreaCode("AC", "1200401")).toBe(68); + }); +}); diff --git a/src/_internals/read-municipality-area-code/read-municipality-area-code.ts b/src/_internals/read-municipality-area-code/read-municipality-area-code.ts new file mode 100644 index 000000000..ba5cf83d6 --- /dev/null +++ b/src/_internals/read-municipality-area-code/read-municipality-area-code.ts @@ -0,0 +1,35 @@ +import { DATA as CITIES_DATA } from "../constants/municipalities"; +import { MUNICIPALITY_AREA_CODES } from "../constants/municipality-area-codes"; +import { type StateCode } from "../constants/states"; + +const AREA_CODE_LENGTH = 2; + +const sortedCodes = new Map(); + +/** + * Reads the DDD of a municipality out of `MUNICIPALITY_AREA_CODES`, which holds the DDDs of each + * state in ascending order of the municipality code. The sorted codes of each state are built on + * its first lookup and kept. + * + * @param {StateCode} stateCode - The state of the municipality. + * @param {string} code - The 7 digit IBGE code of a municipality of that state. + * @returns {number} The DDD of the municipality. + * + * @example + * ```typescript + * readMunicipalityAreaCode("SP", "3550308"); // 11 + * ``` + */ +export const readMunicipalityAreaCode = (stateCode: StateCode, code: string): number => { + let codes = sortedCodes.get(stateCode); + + // Stryker disable next-line ConditionalExpression: this guard only memoizes; CITIES_DATA is a module level constant that is never written to, so sorting the codes again on every lookup gives the same array, and the repeated work is unobservable. + if (codes === undefined) { + codes = CITIES_DATA[stateCode].map(([, municipalityCode]) => municipalityCode).sort(); + sortedCodes.set(stateCode, codes); + } + + const start = codes.indexOf(code) * AREA_CODE_LENGTH; + + return Number(MUNICIPALITY_AREA_CODES[stateCode].slice(start, start + AREA_CODE_LENGTH)); +}; diff --git a/src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.test.ts b/src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.test.ts new file mode 100644 index 000000000..383c94295 --- /dev/null +++ b/src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.test.ts @@ -0,0 +1,77 @@ +import * as fc from "fast-check"; + +import { VALID_AREA_CODES } from "../_internals/constants/area-codes"; +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { getAreaCodeInfo } from "../get-area-code-info/get-area-code-info"; +import { getMunicipalities } from "../get-municipalities/get-municipalities"; +import { getAreaCodeByMunicipalityCode } from "./get-area-code-by-municipality-code"; + +describe("getAreaCodeByMunicipalityCode", () => { + it("should return the DDD of a municipality, per the Anatel Códigos Nacionais table", () => { + expect(getAreaCodeByMunicipalityCode("3550308")).toBe(11); + expect(getAreaCodeByMunicipalityCode("3509502")).toBe(19); + expect(getAreaCodeByMunicipalityCode("3304557")).toBe(21); + expect(getAreaCodeByMunicipalityCode("1200401")).toBe(68); + expect(getAreaCodeByMunicipalityCode("2605459")).toBe(81); + }); + + it("should take the code as a number", () => { + expect(getAreaCodeByMunicipalityCode(3_518_800)).toBe(11); + }); + + it("should ignore whitespace and hyphens, the same as getMunicipalityByCode", () => { + expect(getAreaCodeByMunicipalityCode(" 355-030-8 ")).toBe(11); + }); + + it("should give 61 to Brasília and to the Goiás municipalities around it", () => { + expect(getAreaCodeByMunicipalityCode("5300108")).toBe(61); + expect(getAreaCodeByMunicipalityCode("5212501")).toBe(61); + expect(getAreaCodeByMunicipalityCode("5222203")).toBe(61); + }); + + it("should give the three municipalities that dial a neighboring state's DDD that DDD", () => { + expect(getAreaCodeByMunicipalityCode("4213609")).toBe(42); + expect(getAreaCodeByMunicipalityCode("4122305")).toBe(47); + expect(getAreaCodeByMunicipalityCode("4102604")).toBe(49); + }); + + it("should return null for a code that is not a municipality", () => { + expect(getAreaCodeByMunicipalityCode("0000000")).toBeNull(); + expect(getAreaCodeByMunicipalityCode("355030")).toBeNull(); + expect(getAreaCodeByMunicipalityCode("")).toBeNull(); + }); + + it("should return null for a code with a letter, a negative or a fractional number", () => { + expect(getAreaCodeByMunicipalityCode("355030a8")).toBeNull(); + expect(getAreaCodeByMunicipalityCode(-3_550_308)).toBeNull(); + expect(getAreaCodeByMunicipalityCode(355_030.8)).toBeNull(); + }); + + describe("properties", () => { + test("should never throw, regardless of the input", () => { + expectNeverThrows(getAreaCodeByMunicipalityCode, anyGarbage); + }); + + test("should give every municipality a DDD in use that lists its state", () => { + const municipalityArbitrary = fc.constantFrom(...getMunicipalities()); + + fc.assert( + fc.property(municipalityArbitrary, (municipality) => { + const areaCode = getAreaCodeByMunicipalityCode(municipality.code); + + expect(VALID_AREA_CODES).toContain(areaCode); + expect(getAreaCodeInfo(areaCode ?? 0)?.stateCodes).toContain(municipality.stateCode); + }), + ); + }); + }); +}); + +describe("getAreaCodeByMunicipalityCode types", () => { + test("should take a string or number and return a number or null", () => { + expectTypeOf(getAreaCodeByMunicipalityCode).parameter(0).toEqualTypeOf(); + expectTypeOf(getAreaCodeByMunicipalityCode).returns.toEqualTypeOf(); + }); +}); diff --git a/src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.ts b/src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.ts new file mode 100644 index 000000000..1796f133c --- /dev/null +++ b/src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.ts @@ -0,0 +1,39 @@ +import { readMunicipalityAreaCode } from "../_internals/read-municipality-area-code/read-municipality-area-code"; +import { getMunicipalityByCode } from "../get-municipality-by-code/get-municipality-by-code"; + +/** + * Looks up the DDD (area code, the Código Nacional of the Plano Geral de Numeração) in force for + * a Brazilian municipality, given its 7 digit IBGE code. + * + * The code is read the way `getMunicipalityByCode` reads it: a string of digits that may carry + * whitespace and hyphens, or a non-negative integer number. Every one of the 5,571 municipalities + * has exactly one DDD, so `null` only means the code is not a municipality. + * + * A DDD mostly follows state lines, with four exceptions: 61, the DDD of Brasília, also covers + * 12 municipalities of Goiás around it, and three municipalities dial the DDD of the + * neighboring state, Rio Negro (PR) 47, Barracão (PR) 49 and Porto União (SC) 42. + * `getAreaCodeInfo` gives the state and region the DDD is seated in. + * + * @param {string|number} code - The 7 digit IBGE municipality code. + * @returns {number|null} The DDD, or `null` when `code` is not the code of a municipality. + * + * @example + * ```typescript + * getAreaCodeByMunicipalityCode("3550308"); // 11 (São Paulo/SP) + * getAreaCodeByMunicipalityCode(3304557); // 21 (Rio de Janeiro/RJ) + * getAreaCodeByMunicipalityCode("4122305"); // 47 (Rio Negro/PR) + * getAreaCodeByMunicipalityCode("0000000"); // null + * ``` + * + * @see Official: https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais + * Anatel, Painel de Dados de Áreas Tarifárias, "Códigos Nacionais". + * @see Official: https://www.anatel.gov.br/dadosabertos/paineis_de_dados/areastarifarias/pgcn.zip + * `Codigos_Nacionais.csv` of 21/09/2026, the Código Nacional of every municipality in force. + */ +export const getAreaCodeByMunicipalityCode = (code: string | number): number | null => { + const municipality = getMunicipalityByCode(code); + + if (municipality === null) return null; + + return readMunicipalityAreaCode(municipality.stateCode, municipality.code); +}; diff --git a/src/get-municipalities-by-area-code/get-municipalities-by-area-code.test.ts b/src/get-municipalities-by-area-code/get-municipalities-by-area-code.test.ts new file mode 100644 index 000000000..aa0067afb --- /dev/null +++ b/src/get-municipalities-by-area-code/get-municipalities-by-area-code.test.ts @@ -0,0 +1,112 @@ +import * as fc from "fast-check"; + +import { VALID_AREA_CODES } from "../_internals/constants/area-codes"; +import { type Municipality } from "../_internals/constants/municipalities"; +import { anyGarbage } from "../_internals/test/arbitraries"; +import { expectNeverThrows } from "../_internals/test/properties"; +import { describe, expect, expectTypeOf, it, test } from "../_internals/test/runtime"; +import { getAreaCodeByMunicipalityCode } from "../get-area-code-by-municipality-code/get-area-code-by-municipality-code"; +import { getMunicipalities } from "../get-municipalities/get-municipalities"; +import { getMunicipalitiesByAreaCode } from "./get-municipalities-by-area-code"; + +describe("getMunicipalitiesByAreaCode", () => { + it("should list every municipality of Acre for 68", () => { + expect(getMunicipalitiesByAreaCode(68)).toEqual(getMunicipalities("AC")); + }); + + it("should list the 64 municipalities of DDD 11, per the Anatel Códigos Nacionais table", () => { + const municipalities = getMunicipalitiesByAreaCode("11"); + + expect(municipalities).toHaveLength(64); + expect(municipalities).toContainEqual({ code: "3550308", name: "São Paulo", stateCode: "SP" }); + expect(municipalities).toContainEqual({ code: "3518800", name: "Guarulhos", stateCode: "SP" }); + expect(municipalities).not.toContainEqual({ + code: "3509502", + name: "Campinas", + stateCode: "SP", + }); + }); + + it("should sort each state's municipalities by name, the way getMunicipalities does", () => { + const names = getMunicipalitiesByAreaCode(11).map(({ name }) => name); + + expect(names).toEqual([...names].sort((a, b) => a.localeCompare(b, "pt-BR"))); + }); + + it("should list Brasília first for 61, then the 12 Goiás municipalities around it", () => { + const municipalities = getMunicipalitiesByAreaCode("(61)"); + + expect(municipalities).toHaveLength(13); + expect(municipalities[0]).toEqual({ code: "5300108", name: "Brasília", stateCode: "DF" }); + expect(municipalities.slice(1).every(({ stateCode }) => stateCode === "GO")).toBe(true); + }); + + it("should list the one municipality across a state line after the seat state's", () => { + expect(getMunicipalitiesByAreaCode(42).at(-1)).toEqual({ + code: "4213609", + name: "Porto União", + stateCode: "SC", + }); + expect(getMunicipalitiesByAreaCode(47).at(-1)).toEqual({ + code: "4122305", + name: "Rio Negro", + stateCode: "PR", + }); + expect(getMunicipalitiesByAreaCode(49).at(-1)).toEqual({ + code: "4102604", + name: "Barracão", + stateCode: "PR", + }); + }); + + it("should return fresh objects, so mutating one leaves a later call intact", () => { + const [first] = getMunicipalitiesByAreaCode(68); + + if (first !== undefined) first.name = "MUTATED"; + + expect(getMunicipalitiesByAreaCode(68)[0]?.name).toBe("Acrelândia"); + }); + + it("should return an empty array for a DDD that is not in use, or for input that is not a DDD", () => { + expect(getMunicipalitiesByAreaCode("20")).toEqual([]); + expect(getMunicipalitiesByAreaCode("1e1")).toEqual([]); + expect(getMunicipalitiesByAreaCode(-11)).toEqual([]); + expect(getMunicipalitiesByAreaCode("")).toEqual([]); + }); + + describe("properties", () => { + test("should never throw, regardless of the input", () => { + expectNeverThrows(getMunicipalitiesByAreaCode, anyGarbage); + }); + + test("should agree with getAreaCodeByMunicipalityCode on every DDD", () => { + fc.assert( + fc.property(fc.constantFrom(...VALID_AREA_CODES), (areaCode) => { + const municipalities = getMunicipalitiesByAreaCode(areaCode); + + expect(municipalities.length).toBeGreaterThan(0); + + for (const { code } of municipalities) { + expect(getAreaCodeByMunicipalityCode(code)).toBe(areaCode); + } + }), + ); + }); + + test("should list every municipality under exactly one DDD", () => { + const listed = VALID_AREA_CODES.flatMap((areaCode) => + getMunicipalitiesByAreaCode(areaCode).map(({ code }) => code), + ); + + expect(listed).toHaveLength(getMunicipalities().length); + expect(new Set(listed).size).toBe(listed.length); + }); + }); +}); + +describe("getMunicipalitiesByAreaCode types", () => { + test("should take a string or number and return Municipality[]", () => { + expectTypeOf(getMunicipalitiesByAreaCode).parameter(0).toEqualTypeOf(); + expectTypeOf(getMunicipalitiesByAreaCode).returns.toEqualTypeOf(); + }); +}); diff --git a/src/get-municipalities-by-area-code/get-municipalities-by-area-code.ts b/src/get-municipalities-by-area-code/get-municipalities-by-area-code.ts new file mode 100644 index 000000000..73f1c702a --- /dev/null +++ b/src/get-municipalities-by-area-code/get-municipalities-by-area-code.ts @@ -0,0 +1,45 @@ +import { DATA as CITIES_DATA, type Municipality } from "../_internals/constants/municipalities"; +import { readMunicipalityAreaCode } from "../_internals/read-municipality-area-code/read-municipality-area-code"; +import { getAreaCodeInfo } from "../get-area-code-info/get-area-code-info"; + +export type { Municipality } from "../_internals/constants/municipalities"; + +/** + * Lists the Brazilian municipalities that dial a given DDD (area code, the Código Nacional of the + * Plano Geral de Numeração). + * + * The DDD is read the way `getAreaCodeInfo` reads it: a string that may wrap it in parentheses + * and carry whitespace and hyphens, or a non-negative integer number. The municipalities come + * state by state, the state the DDD is seated in first and then the others of + * `AreaCodeInfo.stateCodes`, each state's sorted by name the way `getMunicipalities` sorts them. + * Four DDDs cross a state line: 61 also covers 12 municipalities of Goiás around Brasília, 42 + * covers Porto União (SC), 47 covers Rio Negro (PR) and 49 covers Barracão (PR). + * + * @param {string|number} areaCode - The DDD. + * @returns {Municipality[]} A fresh array of fresh `{ code, name, stateCode }` objects, empty + * when `areaCode` is not one of the 67 DDDs in use. + * + * @example + * ```typescript + * getMunicipalitiesByAreaCode(68).length; // 22 (every municipality of Acre) + * getMunicipalitiesByAreaCode("47").at(-1); // { code: "4122305", name: "Rio Negro", stateCode: "PR" } + * getMunicipalitiesByAreaCode("(61)").length; // 13 (Brasília and 12 municipalities of Goiás) + * getMunicipalitiesByAreaCode("20"); // [] + * ``` + * + * @see Official: https://informacoes.anatel.gov.br/paineis/areas-tarifarias/codigos-nacionais + * Anatel, Painel de Dados de Áreas Tarifárias, "Códigos Nacionais". + * @see Official: https://www.anatel.gov.br/dadosabertos/paineis_de_dados/areastarifarias/pgcn.zip + * `Codigos_Nacionais.csv` of 21/09/2026, the Código Nacional of every municipality in force. + */ +export const getMunicipalitiesByAreaCode = (areaCode: string | number): Municipality[] => { + const info = getAreaCodeInfo(areaCode); + + if (info === null) return []; + + return info.stateCodes.flatMap((stateCode) => + CITIES_DATA[stateCode] + .filter(([, code]) => readMunicipalityAreaCode(stateCode, code) === info.areaCode) + .map(([name, code]) => ({ code, name, stateCode })), + ); +}; diff --git a/src/index.test.ts b/src/index.test.ts index 0f4625d49..9c2144a3c 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -185,6 +185,7 @@ const PUBLIC = [ "generateSuframa", "generateVoterId", "getAddressInfoByCep", + "getAreaCodeByMunicipalityCode", "getAreaCodeInfo", "getAreaCodesByState", "getBankByCode", @@ -211,6 +212,7 @@ const PUBLIC = [ "getLegalNatures", "getLegalNaturesByCategory", "getMunicipalities", + "getMunicipalitiesByAreaCode", "getMunicipality", "getMunicipalityByCode", "getNbs", diff --git a/src/index.ts b/src/index.ts index 59afeb2d7..cab25b8cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -92,6 +92,7 @@ export { GetAddressInfoByCepValidationError, getAddressInfoByCep, } from "./get-address-info-by-cep/get-address-info-by-cep"; +export { getAreaCodeByMunicipalityCode } from "./get-area-code-by-municipality-code/get-area-code-by-municipality-code"; export { type AreaCodeInfo, getAreaCodeInfo } from "./get-area-code-info/get-area-code-info"; export { getAreaCodesByState } from "./get-area-codes-by-state/get-area-codes-by-state"; export { getBankByCode } from "./get-bank-by-code/get-bank-by-code"; @@ -153,6 +154,7 @@ export { getLegalNaturesByCategory, } from "./get-legal-natures-by-category/get-legal-natures-by-category"; export { getMunicipalities } from "./get-municipalities/get-municipalities"; +export { getMunicipalitiesByAreaCode } from "./get-municipalities-by-area-code/get-municipalities-by-area-code"; export { type GetMunicipalityByCodeParams, type GetMunicipalityByNameParams, From 71acedec598930a15790b955c755678672049729 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 16:24:17 +0000 Subject: [PATCH 2/4] perf(area-codes): index each state's DDDs once instead of searching per municipality getMunicipalitiesByAreaCode found each municipality's position with indexOf over the state's sorted codes, so listing Minas Gerais compared about 360,000 strings, and the property test that also looked every listed municipality up again (5,571 linear lookups through getMunicipalityByCode) timed out at 5 seconds in the Stryker dry run. readMunicipalityAreaCode now builds each state's code-to-DDD index on its first lookup and keeps it, and the property test samples municipalities instead of walking all of them; a separate test still checks every DDD in use lists a municipality. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH --- .../read-municipality-area-code.ts | 29 ++++++++++++------- .../get-municipalities-by-area-code.test.ts | 20 ++++++++----- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/_internals/read-municipality-area-code/read-municipality-area-code.ts b/src/_internals/read-municipality-area-code/read-municipality-area-code.ts index ba5cf83d6..51f081440 100644 --- a/src/_internals/read-municipality-area-code/read-municipality-area-code.ts +++ b/src/_internals/read-municipality-area-code/read-municipality-area-code.ts @@ -4,12 +4,12 @@ import { type StateCode } from "../constants/states"; const AREA_CODE_LENGTH = 2; -const sortedCodes = new Map(); +const indexes = new Map>(); /** * Reads the DDD of a municipality out of `MUNICIPALITY_AREA_CODES`, which holds the DDDs of each - * state in ascending order of the municipality code. The sorted codes of each state are built on - * its first lookup and kept. + * state in ascending order of the municipality code. The DDDs of each state are indexed by + * municipality code on its first lookup, and the index is kept. * * @param {StateCode} stateCode - The state of the municipality. * @param {string} code - The 7 digit IBGE code of a municipality of that state. @@ -21,15 +21,22 @@ const sortedCodes = new Map(); * ``` */ export const readMunicipalityAreaCode = (stateCode: StateCode, code: string): number => { - let codes = sortedCodes.get(stateCode); + let index = indexes.get(stateCode); - // Stryker disable next-line ConditionalExpression: this guard only memoizes; CITIES_DATA is a module level constant that is never written to, so sorting the codes again on every lookup gives the same array, and the repeated work is unobservable. - if (codes === undefined) { - codes = CITIES_DATA[stateCode].map(([, municipalityCode]) => municipalityCode).sort(); - sortedCodes.set(stateCode, codes); - } + // Stryker disable next-line ConditionalExpression: this guard only memoizes; CITIES_DATA and MUNICIPALITY_AREA_CODES are module level constants that are never written to, so rebuilding the index on every lookup gives the same entries, and the repeated work is unobservable. + if (index === undefined) { + const areaCodes = MUNICIPALITY_AREA_CODES[stateCode]; + const codes = CITIES_DATA[stateCode].map(([, municipalityCode]) => municipalityCode).sort(); + + index = Object.fromEntries( + codes.map((municipalityCode, position) => { + const start = position * AREA_CODE_LENGTH; - const start = codes.indexOf(code) * AREA_CODE_LENGTH; + return [municipalityCode, Number(areaCodes.slice(start, start + AREA_CODE_LENGTH))]; + }), + ); + indexes.set(stateCode, index); + } - return Number(MUNICIPALITY_AREA_CODES[stateCode].slice(start, start + AREA_CODE_LENGTH)); + return index[code]; }; diff --git a/src/get-municipalities-by-area-code/get-municipalities-by-area-code.test.ts b/src/get-municipalities-by-area-code/get-municipalities-by-area-code.test.ts index aa0067afb..4e88da5d2 100644 --- a/src/get-municipalities-by-area-code/get-municipalities-by-area-code.test.ts +++ b/src/get-municipalities-by-area-code/get-municipalities-by-area-code.test.ts @@ -79,20 +79,24 @@ describe("getMunicipalitiesByAreaCode", () => { expectNeverThrows(getMunicipalitiesByAreaCode, anyGarbage); }); - test("should agree with getAreaCodeByMunicipalityCode on every DDD", () => { - fc.assert( - fc.property(fc.constantFrom(...VALID_AREA_CODES), (areaCode) => { - const municipalities = getMunicipalitiesByAreaCode(areaCode); + test("should list a municipality under the DDD getAreaCodeByMunicipalityCode gives it", () => { + const municipalityArbitrary = fc.constantFrom(...getMunicipalities()); - expect(municipalities.length).toBeGreaterThan(0); + fc.assert( + fc.property(municipalityArbitrary, (municipality) => { + const areaCode = getAreaCodeByMunicipalityCode(municipality.code) ?? 0; - for (const { code } of municipalities) { - expect(getAreaCodeByMunicipalityCode(code)).toBe(areaCode); - } + expect(getMunicipalitiesByAreaCode(areaCode)).toContainEqual(municipality); }), ); }); + test("should list at least one municipality for every DDD in use", () => { + for (const areaCode of VALID_AREA_CODES) { + expect(getMunicipalitiesByAreaCode(areaCode).length).toBeGreaterThan(0); + } + }); + test("should list every municipality under exactly one DDD", () => { const listed = VALID_AREA_CODES.flatMap((areaCode) => getMunicipalitiesByAreaCode(areaCode).map(({ code }) => code), From feeabca9203812ce056e98025c385f7b4f421e0b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 02:04:13 +0000 Subject: [PATCH 3/4] fix(area-codes): read codes by their digits, as getAreaCodeInfo and getMunicipalityByCode do Follows #596 restoring the 2.4.0 contract of the lookups it builds on: a string code has any non-digit character stripped, so the tests and docs no longer expect "1e1" or "355030a8" to be rejected. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH --- docs/pt-br/utilities.md | 4 ++-- docs/utilities.md | 4 ++-- .../get-area-code-by-municipality-code.test.ts | 8 ++++++-- .../get-municipalities-by-area-code.test.ts | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/pt-br/utilities.md b/docs/pt-br/utilities.md index 5ed20311e..f5ca6c0ee 100644 --- a/docs/pt-br/utilities.md +++ b/docs/pt-br/utilities.md @@ -976,7 +976,7 @@ Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legis Retorna o DDD (código de área) de um município brasileiro a partir do código IBGE de 7 dígitos, segundo a tabela da Anatel dos Códigos Nacionais em vigor. -- Aceita o código como o `getMunicipalityByCode`: string (ignorando espaços e hífens) ou número inteiro não negativo. +- Aceita o código como o `getMunicipalityByCode`: string (com todo caractere que não é dígito removido) ou número inteiro não negativo. - Retorna o DDD como número, ou `null` quando o código não é de um município. Cada um dos 5.571 municípios tem exatamente um DDD. - O DDD quase sempre segue a divisa dos estados. As exceções: o 61 também cobre 12 municípios de Goiás no entorno de Brasília, e Porto União (SC) usa o 42, Rio Negro (PR) o 47 e Barracão (PR) o 49. @@ -1043,7 +1043,7 @@ Fonte: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legis Lista os municípios brasileiros que usam um DDD (código de área), segundo a tabela da Anatel dos Códigos Nacionais em vigor. -- Aceita o DDD como o `getAreaCodeInfo`: string (com parênteses, espaços e hífens) ou número inteiro não negativo. +- Aceita o DDD como o `getAreaCodeInfo`: string (com todo caractere que não é dígito removido) ou número inteiro não negativo. - Retorna um array de `{ code, name, stateCode }` (`Municipality`): primeiro os municípios do estado sede, depois os do outro estado em que o DDD entra, cada estado em ordem alfabética. Retorna `[]` quando o DDD não está em uso. ```javascript diff --git a/docs/utilities.md b/docs/utilities.md index dfda59293..61d480d27 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -976,7 +976,7 @@ Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legi Get the DDD (area code) a Brazilian municipality dials, given its 7-digit IBGE code, from the Anatel table of the Códigos Nacionais in force. -- Accepts the code the way `getMunicipalityByCode` does: a string (spaces and hyphens ignored) or a non-negative integer. +- Accepts the code the way `getMunicipalityByCode` does: a string (any non-digit characters stripped) or a non-negative integer. - Returns the DDD as a number, or `null` when the code is not a municipality. Every one of the 5,571 municipalities has exactly one DDD. - A DDD mostly follows state lines. The exceptions: 61 also covers 12 municipalities of Goiás around Brasília, and Porto União (SC) dials 42, Rio Negro (PR) 47 and Barracão (PR) 49. @@ -1043,7 +1043,7 @@ Source: [Resolução Anatel nº 749/2022](https://informacoes.anatel.gov.br/legi List the Brazilian municipalities that dial a given DDD (area code), from the Anatel table of the Códigos Nacionais in force. -- Accepts the DDD the way `getAreaCodeInfo` does: a string (parentheses, spaces and hyphens allowed) or a non-negative integer. +- Accepts the DDD the way `getAreaCodeInfo` does: a string (any non-digit characters stripped) or a non-negative integer. - Returns an array of `{ code, name, stateCode }` (`Municipality`): the seat state's municipalities first, then those of the other state the DDD crosses into, each state's sorted by name. Returns `[]` when the DDD is not in use. ```javascript diff --git a/src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.test.ts b/src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.test.ts index 383c94295..aea6426bb 100644 --- a/src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.test.ts +++ b/src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.test.ts @@ -43,8 +43,12 @@ describe("getAreaCodeByMunicipalityCode", () => { expect(getAreaCodeByMunicipalityCode("")).toBeNull(); }); - it("should return null for a code with a letter, a negative or a fractional number", () => { - expect(getAreaCodeByMunicipalityCode("355030a8")).toBeNull(); + it("should strip any non-digit character of a string code, as getMunicipalityByCode does", () => { + expect(getAreaCodeByMunicipalityCode("3550308 SP")).toBe(11); + }); + + it("should return null for a code with no digit, a negative or a fractional number", () => { + expect(getAreaCodeByMunicipalityCode("SP")).toBeNull(); expect(getAreaCodeByMunicipalityCode(-3_550_308)).toBeNull(); expect(getAreaCodeByMunicipalityCode(355_030.8)).toBeNull(); }); diff --git a/src/get-municipalities-by-area-code/get-municipalities-by-area-code.test.ts b/src/get-municipalities-by-area-code/get-municipalities-by-area-code.test.ts index 4e88da5d2..d2b6e6304 100644 --- a/src/get-municipalities-by-area-code/get-municipalities-by-area-code.test.ts +++ b/src/get-municipalities-by-area-code/get-municipalities-by-area-code.test.ts @@ -69,7 +69,7 @@ describe("getMunicipalitiesByAreaCode", () => { it("should return an empty array for a DDD that is not in use, or for input that is not a DDD", () => { expect(getMunicipalitiesByAreaCode("20")).toEqual([]); - expect(getMunicipalitiesByAreaCode("1e1")).toEqual([]); + expect(getMunicipalitiesByAreaCode("DDD")).toEqual([]); expect(getMunicipalitiesByAreaCode(-11)).toEqual([]); expect(getMunicipalitiesByAreaCode("")).toEqual([]); }); From 62f27ef1980db8ed403392903df621e9bd15a1de Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 02:04:31 +0000 Subject: [PATCH 4/4] docs(area-codes): say the codes are read by their digits Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH --- .../get-area-code-by-municipality-code.ts | 4 ++-- .../get-municipalities-by-area-code.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.ts b/src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.ts index 1796f133c..27f2d1b8d 100644 --- a/src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.ts +++ b/src/get-area-code-by-municipality-code/get-area-code-by-municipality-code.ts @@ -5,8 +5,8 @@ import { getMunicipalityByCode } from "../get-municipality-by-code/get-municipal * Looks up the DDD (area code, the Código Nacional of the Plano Geral de Numeração) in force for * a Brazilian municipality, given its 7 digit IBGE code. * - * The code is read the way `getMunicipalityByCode` reads it: a string of digits that may carry - * whitespace and hyphens, or a non-negative integer number. Every one of the 5,571 municipalities + * The code is read the way `getMunicipalityByCode` reads it: a string, with any non-digit + * characters stripped, or a non-negative integer number. Every one of the 5,571 municipalities * has exactly one DDD, so `null` only means the code is not a municipality. * * A DDD mostly follows state lines, with four exceptions: 61, the DDD of Brasília, also covers diff --git a/src/get-municipalities-by-area-code/get-municipalities-by-area-code.ts b/src/get-municipalities-by-area-code/get-municipalities-by-area-code.ts index 73f1c702a..c34a8e39c 100644 --- a/src/get-municipalities-by-area-code/get-municipalities-by-area-code.ts +++ b/src/get-municipalities-by-area-code/get-municipalities-by-area-code.ts @@ -8,8 +8,8 @@ export type { Municipality } from "../_internals/constants/municipalities"; * Lists the Brazilian municipalities that dial a given DDD (area code, the Código Nacional of the * Plano Geral de Numeração). * - * The DDD is read the way `getAreaCodeInfo` reads it: a string that may wrap it in parentheses - * and carry whitespace and hyphens, or a non-negative integer number. The municipalities come + * The DDD is read the way `getAreaCodeInfo` reads it: a string, with any non-digit characters + * stripped, or a non-negative integer number. The municipalities come * state by state, the state the DDD is seated in first and then the others of * `AreaCodeInfo.stateCodes`, each state's sorted by name the way `getMunicipalities` sorts them. * Four DDDs cross a state line: 61 also covers 12 municipalities of Goiás around Brasília, 42