RÚIAN address points — a zipped CSV-per-municipality export¶
What
The Czech state address register (RÚIAN) is published as a single ZIP
holding one Windows-1250, semicolon-delimited CSV per municipality, all under a
CSV/ folder inside the archive. bxp unpacks every member to a flat
intermediate CSV, transcodes Windows-1250 → UTF-8, keeps the date part of the
Platí Od timestamp, and rolls all municipalities into one clean
1-ruian_adr-combined.csvx ready for a Postgres COPY — from the raw .zip,
in one run, no pre-unzip step.
Why interesting¶
Government open-data registers ship as a zip of many
encoded CSVs, not one tidy UTF-8 file: a legacy code page, a European
delimiter, an in-archive folder prefix, and hundreds-to-thousands of members.
bxp's zip_input pre-pass turns that whole archive into the input — and it
unpacks the members in parallel, so on the full national dataset the unzip
step is the opposite of a bottleneck (numbers below).
flowchart LR
Z["**sample.zip**<br/><small>CSV/ · Windows-1250 · ';'</small>"]
Z -->|"zip_input parallel unpack"| M1["OB_500101_ADR.csv"]
Z --> M2["OB_…_ADR.csv"]
Z --> M3["OB_…_ADR.csv"]
M1 -->|"transcode · date trim"| C["**1-ruian_adr-combined.csvx**<br/><small>UTF-8 · combined_output</small>"]
M2 --> C
M3 --> C
Problem class documented in. RÚIAN (Registr územní identifikace, adres a
nemovitostí) is published by ČÚZK — the Czech Office for Surveying, Mapping and
Cadastre — as open data via the VDP portal
(https://vdp.cuzk.cz/vdp/ruian/vymennyformat). The address-points export
*_OB_ADR_csv.zip carries one OB_<obec>_ADR.csv per municipality under a
CSV/ directory, in Windows-1250 with ; separators — the shape reproduced by
sample.zip here (5 real municipalities, 2 address points each).
The trick¶
Three template keys do the archive handling, no code:
zip_input: { entry_pattern: ".csv" }— before the main loop, unpack every*.zipindata_dir.dir_modedefaults tobasename, which flattens the in-archiveCSV/20260531_OB_500101_ADR.csvpath to a bare filename (keep_pathwould instead join the path with a separator). The unpack is parallelised across CPU cores — independent members, one inflate window per worker.csv_input_encoding: "windows-1250"+csv_delimiter_in: ";"— decode each field to UTF-8 and split on;. The output is plain UTF-8 CSV.combined_output: true— additionally append every file's rows into one merged1-ruian_adr-combined.csvx.
Plus a date trim — DATE_CONVERT([Platí Od], 'YYYY-MM-DD[*]', 'YYYY-MM-DD') —
where [*] swallows the T00:00:00 time suffix, keeping the ISO date.
Final result¶
The raw member is Windows-1250 with a CSV/ prefix and a
timestamp:
CSV/20260531_OB_500101_ADR.csv (Windows-1250, ';'-delimited)
11915692;500101;Bra\x9eec;…;…;36471;835804.84;1019584.24;2016-01-01T00:00:00
becomes one clean UTF-8 row, diacritics and all, date trimmed:
adm_code,municipality_code,municipality,municipality_part,street,house_number,orientation_number,postcode,coord_y,coord_x,valid_from
11915692,500101,Bražec,Dolní Valov,,1,,36471,835804.84,1019584.24,2016-01-01
At full scale¶
The committed sample.zip is a 5-municipality teaching slice; the full national
export is the identical shape three thousand times over — 6,258 municipalities
= 354 MB of CSV inside a 63 MB deflate archive, yielding 3,020,222
address rows in the combined output (2026-07 export).
On an 8-core desktop (shipped ReleaseSmall build) bxp converts it end to end
straight from the .zip — parallel unpack, Windows-1250 transcode, date trim
and combined roll-up — in ~10 s at a flat ~30 MB RSS: streaming inflate,
one window per worker, no whole-archive or whole-file materialisation.
The parallel unpack is where it pulls ahead of a serial unzip. Measured on its
own on a 4-core / 8-thread laptop (i7-7920HQ) that step runs in ~0.50 s versus
~2.5 s single-threaded (~5×) — 6,258 independent members are embarrassingly
parallel, and a work-stealing job queue load-balances the very uneven
per-municipality sizes.
Reproduce the scale run — the portal is form-driven, so fetch-full.sh probes
the published month-end paths and takes the newest that resolves:
bash fetch-full.sh # downloads <YYYYMMDD>_OB_ADR_csv.zip (~63 MB)
bxp-cli --config full.json # reads the archive directly, no unzip step
Unlike the single-file examples, this run explodes into 6,258 intermediate CSVs
next to the combined output. The script therefore stages the payload in a temp
dir (override with BXP_RUIAN_WORK) and leaves ./full as a symlink to it, so
data_dir: "full" keeps working while none of those files land inside the
source tree — editors and SCM watchers have nothing new to index.
Sample data¶
Run it with bxp-cli --config ./sample.json --template ruian_adr — that one
command unpacks sample.zip into per-municipality CSVs, transcodes and converts
each, and writes the merged 1-ruian_adr-combined.csvx:
{
// RÚIAN address points (OB_ADR): the Czech state address register, exported
// by ČÚZK as a ZIP of one Windows-1250, ';'-delimited CSV per municipality,
// all under a `CSV/` folder inside the archive. `zip_input` unpacks every
// member to a flat intermediate CSV first (the unpack runs in parallel — see
// the readme); the main loop then converts each to a clean UTF-8 table, and
// `combined_output` rolls them into one `1-ruian_adr-combined.csvx` ready for
// a Postgres `COPY`.
conversion_templates: {
ruian_adr: {
data_dir: ".",
// Unpack *.zip in data_dir before processing. `dir_mode: basename`
// (default) flattens the in-archive `CSV/…` prefix to a bare filename;
// entry_pattern ".csv" (default) selects the member CSVs.
zip_input: { entry_pattern: ".csv" },
file_pattern_in: ".csv",
file_pattern_out: ".csvx",
combined_output: true,
// The source CSVs are Windows-1250, semicolon-delimited.
csv_delimiter_in: ";",
csv_input_encoding: "windows-1250",
csv_header_line: 1,
input_schema: {
$adm_code: "[Kód ADM]",
$obec_code: "[Kód obce]",
$obec: "[Název obce]",
$cast_obce: "[Název části obce]",
$ulice: "[Název ulice]",
$cislo_dom: "[Číslo domovní]",
$cislo_or: "[Číslo orientační]",
$psc: "[PSČ]",
$y: "[Souřadnice Y]",
$x: "[Souřadnice X]",
// Platí Od is an ISO datetime; keep the date, drop the time component.
$plati_od: "DATE_CONVERT([Platí Od], 'YYYY-MM-DD[*]', 'YYYY-MM-DD')",
},
// Pass-through: one catch-all rule emits every row with the input_schema
// values (a plain column remap needs no routing).
row_rules: [
{ when: "1 = 1", rows: [ {} ] },
],
output_schema: {
adm_code: "$adm_code",
municipality_code: "$obec_code",
municipality: "$obec",
municipality_part: "$cast_obce",
street: "$ulice",
house_number: "$cislo_dom",
orientation_number: "$cislo_or",
postcode: "$psc",
coord_y: "$y",
coord_x: "$x",
valid_from: "$plati_od",
},
},
},
}
adm_code,municipality_code,municipality,municipality_part,street,house_number,orientation_number,postcode,coord_y,coord_x,valid_from
11915692,500101,Bražec,Dolní Valov,,1,,36471,835804.84,1019584.24,2016-01-01
11915714,500101,Bražec,Javorná,,1,,36471,836931.47,1018119.4,2016-01-01
11916117,500127,Doupovské Hradiště,Činov,,2,,36471,841180.89,1016303.03,2020-01-28
11916125,500127,Doupovské Hradiště,Činov,,1,,36471,841265.43,1016381.57,2020-01-28
16337719,500135,Kozlov,Kozlov,,1001,,78357,526244.57,1121916.23,2016-01-01
16337727,500135,Kozlov,Kozlov,,1002,,78357,526657.49,1122375.64,2016-01-01
16338260,500151,Luboměř pod Strážnou,Luboměř pod Strážnou,,1301,,75362,517027.96,1113512.33,2016-01-01
16338294,500151,Luboměř pod Strážnou,Luboměř pod Strážnou,,1305,,75362,516705.58,1113636.73,2016-01-01
16193954,500160,Město Libavá,Město Libavá,Náměstí,1,,78307,526391.41,1109275.09,2016-03-21
16193962,500160,Město Libavá,Město Libavá,Náměstí,2,,78307,526379.2,1109277.19,2016-03-21
The input is a binary file — sample.zip on GitHub.