Skip to content

RÚIAN address points — a zipped CSV-per-municipality export

View on GitHub

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 *.zip in data_dir. dir_mode defaults to basename, which flattens the in-archive CSV/20260531_OB_500101_ADR.csv path to a bare filename (keep_path would 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 merged 1-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.