Postal Codes Dataset for Brazil, BR

996
Updated:
Files:1
Size:104 MB
Rows:1,232,818
Formats:csv
License:MITCC-BY-4.0

Postal Codes Dataset for Brazil, BR including name of the city, town, or place, various administrative divisions and alternative city names.

Part of a Data Solution

  • Postal Codes Solution

    Every worldwide postal-code dataset in one bundle — the ultimate global reference for precise postal codes.

    Explore →

Premium

You're viewing a free sample

Get the complete dataset — all rows and files — delivered instantly after checkout, with lifetime access to the latest version.

  • Secure checkout via Stripe
  • Instant download after payment
  • Lifetime access to the latest version
  • MIT License license
20% off
$49.90$39.92
one-time payment

API Access

Access dataset files directly from scripts, code, or AI agents.

Browse dataset files
Dataset Files

Each file has a stable URL (r-link) that you can use directly in scripts, apps, or AI agents. These URLs are permanent and safe to hardcode.

/logistics/postal-codes-br/
https://datahub.io/logistics/postal-codes-br/_r/-/.licensing-notes.md
https://datahub.io/logistics/postal-codes-br/_r/-/.migration-notes.md
https://datahub.io/logistics/postal-codes-br/_r/-/ATTRIBUTION.txt
https://datahub.io/logistics/postal-codes-br/_r/-/README.md
https://datahub.io/logistics/postal-codes-br/_r/-/datapackage.yaml
Key Files

Start with these files — they give you everything you need to understand and access the dataset.

datapackage.yaml— metadata & schema
https://datahub.io/logistics/postal-codes-br/_r/-/datapackage.yaml
README.md— documentation
https://datahub.io/logistics/postal-codes-br/_r/-/README.md
Typical Usage
  1. 1. Fetch datapackage.yaml to inspect schema and resources
  2. 2. Download data resources listed in datapackage.yaml
  3. 3. Read README.md for full context

Data Files

postal-codes-br-sample


About this dataset

Brazil (BR): migration notes

Internal record of the P5 migration. This is a dated record (2026-10-12) and is not shipped in the zip.

  • Issue: datopian/postal-codes#41 (pc-28e.7.8, was deferred 2026-10-03)
  • Producer: scripts/produce.py (+ test_produce.py), pipeline: producer: custom, refresh disabled
  • Parity: unexplained by the repo's classifier (every difference is in a class it has no name for), signed off by nobody yet. Against the live zip's br.csv: 1,233,231 live rows, 1,232,818 fresh; 442 rows differ in all, 0.036% (413 only in live, 29 changed). All 442 are traced below.

Why a new producer, not a port of the legacy chain

The old chain (legacy_source_pipeline/brazil_postal_code_parse.py -> refine_raw_source.py -> run_qa.py -> build_postal_codes_product.py, with scrape_admin.py) was run on today's upstream inputs (2026-10-12) and does not reproduce the live file: 1,207,370 rows against 1,233,231, and 31,882 rows (2.59%) differ. Run from the saved prototype intermediates it was byte-identical to live (checked earlier in the session), so the committed code is not the code that produced the live file. Measured stage by stage:

StageSaved (what built live)Rerun on fresh inputsDifference
parse -> final_output -> raw.csv1,233,231 rows1,232,991240 CEPs only in saved; 0 only in fresh
raw.csv cells, common CEPscounty 29,754 (23,678 now empty); stateName 1,946 (all empty); cityName 298; ibgeCode 3; additionalInfo all 1,232,991 (see below)
Brazil.csv (QA)1,233,2321,207,39125,861 only in saved = 25,621 dropped by QA as "county must not be empty" + the 240
13-column br.csv vs live1,233,2311,207,37025,861 only in live; 6,021 changed (admin_name2 6,017, admin_code2 2,725 now empty, place_name 64)

Causes, each checked against the inputs:

  • The committed parse blanks the municipality for every CEP that is only in the Mendeley list (23,505 live CEPs). combine_clean_data() moves the Mendeley/ruacep city into streetName and blanks cityName; the merge keeps the blank because combine_first treats "" as present. The live file has the municipality on all of them.
  • additionalInfo CAPITAL / COUNTRYSIDE (520,637 / 712,594 rows in the saved raw.csv) is produced by nothing in the committed code or inputs; the committed parse writes it empty. It is dropped in the 13-column product, so it does not reach the live file.
  • Municipality spelling. The live admin_name2 equals the IBGE Localidades API name for its admin_code2 on 1,233,205 of 1,233,231 rows. The legacy parse copies the source's spelling instead ("Santa Bárbara D'Oeste" vs "Santa Bárbara d'Oeste").
  • run_qa.py calls sys.executable without importing sys, so its Frictionless step could not have run as committed.
  • The old README's claim that the logic was verified SHA256-identical is true only of the saved intermediates, not of a run from upstream.

What the producer does

Rules, per CEP (full list in the produce.py docstring): source is OpenCEP's record, else the first Mendeley row; state from the UF; municipality by IBGE code (OpenCEP carries it; Mendeley rows are placed by UF + folded city name through kelvins, then the IBGE list); admin_name2 is the IBGE API name for the code; place_name is the bairro or the municipality if empty; "A/B" bairros keep "A" and the full name goes to alternative_city_name; control characters are removed from the bairro; ordered by state, municipality, place, CEP. Run in ~35 s from cached inputs.

Inputs (all fetched upstream, data/br.inputs.json records url, time, size, sha256): OpenCEP release 2.0.1 v1.zip (342,302,994 bytes, MIT; it is the only release in the repo, published 2024-07-08), the Mendeley CSV (99,290,030 bytes, CC BY 4.0, last modified 2020-01-03), kelvins municipios.csv (MIT), IBGE Localidades estados and municipios (the API gzips its answers; the producer decompresses). The IBGE DTB report and the ruacep file that the old chain read are not inputs and are removed from the tree (still in git history).

Parity against the live zip (2026-10-12)

rows
live1,233,231
fresh1,232,818
only in live422 (by the repo's key postal_code, place_name; 413 CEPs are gone, 9 changed their place_name)
only in fresh9
changed cells20 (all admin_name2)
deviating rows, by CEP442 = 413 + 29, 0.036% of live

Every other cell, including all 1,232,818 postal_code, admin_name1/code1, admin_code2 and the empty coordinate columns, equals live. max_row_drop is 0.015, the drop is 0.03%.

DifferenceRowsCause
CEP only in live, from the ruacep.com.br scrape173Mineiros GO 103, Picos PI 35, Ubá MG 15, São Pedro da Aldeia RJ 13, Vinhedo SP 7. In neither OpenCEP nor Mendeley. Licence-driven: see below
CEP only in live, in no input at all240All Extrema MG (37646-xxx / 37647-xxx, the Barreiro district). Not in OpenCEP 2.0.1, Mendeley or ruacep. The live file has 194 CEPs starting 37646 and 129 starting 37647; OpenCEP 2.0.1 has 43 and 40. Consistent with the live build having read an older OpenCEP snapshot for this district, but not provable: only one release exists
Extrema rows whose bairro differs3Live Morro Grande / Barreiro, OpenCEP 2.0.1 now Condomínio Itamaraty / Estância Barreiro: source changed (37647-290, -302, -312)
Municipality spelling26Five municipalities: Arês -> Arez (RN), Açu -> Assú (RN), São Luiz -> São Luiz do Anauá (RR), Barão de Monte Alto -> Barão do Monte Alto (MG), Gracho Cardoso -> Graccho Cardoso (SE). The live file has the older spelling kelvins carries; the producer uses IBGE's current name. 6 of the 26 are also the place_name where the bairro was empty
Control character0 nowOpenCEP 37644-390 has Á<U+0081>guas; the live file is clean. The producer removes control characters, so this row matches

Informational: the public sample at postal.datahub.io/br/br.csv is unexplained against the fresh first 100 rows (75 unclassified cells, 14 float_suffix_dropped): it is the old mangled sample.

Verification (2026-10-12)

  • Random 100 rows (seed 20261012), each checked against the raw OpenCEP JSON / Mendeley rows / kelvins / IBGE API by a separate script that imports nothing from the producer: 100 of 100 pass (96 OpenCEP, 4 Mendeley). A second 100 drawn only from Mendeley-placed CEPs (the path the legacy chain broke): 100 of 100 pass.
  • Municipality coverage: 5,570 municipalities have a row in live and in fresh, the same set. IBGE lists 5,571: the one without a CEP in any source (Boa Esperança do Norte, MT) is not in the product, as before.
  • scripts/build.py br, build.py --all --check (248 ok), pytest (1,681 passed; 34 are br). Frictionless run inside the unpacked zip (datapackage.yaml + br.csv): VALID.

Licence: the ruacep.com.br scrape

Decision for the owner; nothing here decides it. cep_data.csv (9,324 lines, 9,149 distinct CEPs) came from ruacep.com.br ("todos os direitos reservados", no reuse licence).

  • Live rows that depend on it: 173 of 1,233,231 = 0.014%. They exist only because of the scrape.
  • 8,951 further live rows have their CEP in the scrape too, but are reproduced identically from OpenCEP and Mendeley (0 differences in any field), so the scrape contributes nothing to them. The "~7,000 rows" in .licensing-notes.md and "roughly 7,000" in ATTRIBUTION.txt are not what the data shows.
  • The producer does not use the scrape, and stays inside the 1% bound without it (-173 rows). The price is losing those 173 CEPs; the product loses no municipality.
  • The scrape file and the code that read it are deleted from the tree by this change (git history keeps them). The shipped README.md ("a small subset") and ATTRIBUTION.txt ("roughly 7,000 rows") still carry the caveat; they were left as they are because the owner has not decided whether to keep, reword or drop it, and the live zip still contains the 173 rows until it is republished.

Decisions

DecisionByBasis
Write the producer from the data's own rules instead of porting the legacy chainAgentThe chain does not reproduce live (2.59% differ); the rules reproduce it to 0.036%
Do not use ruacep.com.brAgent, pending ownerNo reuse licence; costs 173 rows
admin_name2 = IBGE API name by code (26 rows differ from live spelling)AgentMatches live on 1,233,205 of 1,233,231 rows; no single source reproduces the other 26
Remove control characters from the bairroAgentOne row; makes it equal live
Delete legacy_source_pipeline/, refine_raw_source.py, scrape_admin.py, run_qa.py, build_postal_codes_product.py, scripts/README.mdAgentReplaced by produce.py; recoverable from history

Open questions

  • Sign-off of the 442 rows (0.036%) as explained, and of dropping the 173 ruacep rows.
  • Keep, reword or drop the ruacep caveat in README.md / ATTRIBUTION.txt once the 173 rows are gone from the shipped file.
  • The 240 Extrema CEPs: ask SeuAliado whether an older snapshot exists, or accept the loss.
  • Download time and fragility. fetch_inputs buffers the 342 MB OpenCEP zip in memory and restarts it on any failure. GitHub's release CDN was very slow on 2026-10-12 (the first download took over an hour); the old parse used curl --retry -C -. If it keeps failing on the runner, resumable download belongs in scripts/producers/common.py, not here.
  • Mendeley's data stops in 2018 and OpenCEP's only release is 2024-07; refresh stays disabled, so nothing will change until someone re-runs it.