Addresses and Search
Hub ships with a centralized address suggestion API used by every downstream project (Labs, Hub‑app, customer solutions) through dependencies/io-oidis-hub-gui. The API exposes autocomplete endpoints for cities, districts, postal codes, streets, house numbers and full addresses, plus an ARES company lookup. Two storage backends are supported and can be switched at runtime: PostgreSQL (default) and Typesense (optional, full‑text + typo tolerant).
The dataset can be auto‑loaded from the Czech RUIAN registry (Registr územní identifikace, adres a nemovitostí, ČÚZK).
Architecture
[ AddressesPanel + AddressesPanelController (hub-gui) ]
│ asyncInvoke
▼
[ AddressesConnector (hub) ] [ TypesenseConnector (hub) ]
│ pickBackend() → "postgres" / "typesense"
├─→ PostgresContext (default backend, ILIKE + unaccent)
└─→ TypesenseConnector.Search (optional backend, REST API)
[ RUIANSeeder ] Downloader → Unzipper → Parser → batch upsert (PG or TS)
pickBackend() reads AppSettings.AddressBackend ("auto" | "postgres" | "typesense", default "auto") with a 30 s in‑memory cache to avoid per‑keystroke DB roundtrips. When the persisted choice is "auto", Typesense wins if it is reachable; otherwise Postgres is used.
Configuration
Hub commons.jsonp defaults:
dbConfigs: {
typesense: {
location: "", // empty = backend disabled, fall through to Postgres
port: 8108,
pass: "" // X-TYPESENSE-API-KEY (override via private.conf.jsonp)
}
},
ruian: {
// ČÚZK monthly snapshot of full address data; {stamp} is resolved to YYYYMMDD of the last day of the most recent published month.
sourceUrl: "https://vdp.cuzk.gov.cz/vymenny_format/csv/{stamp}_OB_ADR_csv.zip",
cachePath: "<? @var project.target.appDataPath ?>/ruian",
batchSize: 500
}
The default sourceUrl points at ČÚZK's monthly RUIAN snapshot. ČÚZK publishes the country-wide ZIP at the end of each month under vdp.cuzk.gov.cz/vymenny_format/csv/<YYYYMMDD>_OB_ADR_csv.zip (~60 MB, windows-1250 CSV containing full address data — city / part / street names, house and orientation numbers, PSČ). The seeder's resolver supports three URL patterns:
| Pattern | Behaviour |
|---|---|
Direct .zip URL | used as-is (pin a specific snapshot or mirror) |
URL with {stamp} placeholder | replaced with the last day of the current month, then probed via HEAD; falls back up to 6 months if the latest snapshot is not yet published |
| Anything else (Atom feed XML / JSON) | fetched, and the entry with the highest YYYYMMDD stamp from any embedded .zip link is chosen — works for both XML Atom and JSON discovery responses |
To enable Typesense in a downstream deployment, set dbConfigs.typesense.location and pass in private.conf.jsonp and restart Hub. ZIP extraction reuses FileSystemHandler.UnpackAsync (powered by the decompress lib that Hub already ships) and CSV parsing reuses CSVHandler.Import (already wired through csv + iconv-lite in Hub's defaults), so RUIAN seeding adds no new runtime dependency.
Admin UI
The Addresses panel (admin scope, /addresses) exposes:
- Address validation — autocomplete fields for cities/districts/postal codes/streets/house numbers, demonstrating the same RPC API consumers should use.
- Address load from ARES — IČO lookup against
https://ares.gov.czwith a JSON dump. - Backend configuration — Test connection button reports availability and latency for both PostgreSQL and Typesense, and an Active backend text input persists the admin's choice (
postgres/typesense/auto). - Load addresses from RUIAN — Last sync shows the timestamp and row count of the last successful import. Load addresses from RUIAN triggers the seeder on demand; Rebuild Typesense index re‑indexes Postgres data into Typesense without re‑downloading. The Auto‑sync checkbox + interval (hours) configure an unattended cron that re‑runs the import on schedule when the active backend is reachable.
RPC API
| Method | Returns | Notes |
|---|---|---|
SuggestCity / SuggestDistrict / SuggestPostalCode / SuggestStreet / SuggestHouseNumber / SuggestAddress | ISuggestion[] / IDetailedSuggestion[] | Dispatch to the active backend transparently. |
AddressFromIco($ico) | IAddress | ARES lookup. Backend‑agnostic (HTTP). |
SuggestSubjectByName($query, $limit?) | ISubjectSearchResult | ARES subject autocomplete. |
TestPostgresConnection() | IBackendStatus | SELECT version() round‑trip + latency. |
TestTypesenseConnection() | IBackendStatus | Calls Typesense /health. |
GetAddressBackendStatus() | IAddressBackendStatus | Both probes in parallel + persisted choice + row counts. |
SetActiveBackend($kind) | void | Persists "postgres" / "typesense" / "auto" to AppSettings.AddressBackend. |
SetAutoSync($enabled, $intervalHours) | void | Persists the unattended cron schedule to AppSettings.RuianAutoSync + RuianAutoSyncIntervalHours. |
LoadAddressesFromRUIAN($options?, $callback?) | void | Downloads + parses + upserts RUIAN data into the active backend; on success stamps RuianLastSyncAt + RuianLastSyncCount. |
RebuildTypesenseIndex($callback?) | void | Re‑indexes Typesense from current Postgres data. |
Typesense Schema
Collection addresses is denormalized so a single document covers all six Suggest* queries:
fields: [
{ name: "refId", type: "string", index: false },
{ name: "city_code", type: "int64", facet: true },
{ name: "city_name", type: "string", sort: true, locale: "cs" },
{ name: "district_code", type: "int64", facet: true, optional: true },
{ name: "district_name", type: "string", optional: true, locale: "cs" },
{ name: "street_code", type: "int64", facet: true, optional: true },
{ name: "street_name", type: "string", optional: true, locale: "cs" },
{ name: "house_number", type: "string", optional: true },
{ name: "orientation_number", type: "string", optional: true },
{ name: "postal_code", type: "string", facet: true },
{ name: "full_address", type: "string", locale: "cs" }
]
default_sorting_field: "city_code"
token_separators: [".", "-", "/", " "]
RUIAN Seed Flow
RUIANSeeder.SeedAll($backend, $options, $progress) executes:
- Resolve & download — when
sourceUrlis not a direct.zip,RUIANSeederfetches it (ČÚZK Atom feed by default), picks the link with the highestYYYYMMDDstamp, and downloads it viaFileSystemHandler.DownloadAsyncintodbConfigs.ruian.cachePath. Idempotent; passforceRefresh: trueto overwrite. - Unpack —
FileSystemHandler.UnpackAsync($archive, {output: extractDir})extracts every entry; CSV files are then enumerated viaFileSystemHandler.Expand("**/*.csv"). - Parse — each CSV runs through
CSVHandler.Import($path, {encoding: "win1250"}), which already handles delimiter detection, header row, and Windows‑1250 → UTF‑8 transcoding through Hub's existingcsv+iconv-litedev dependencies. - Upsert — rows are sliced into batches of
dbConfigs.ruian.batchSize. Postgres path issuesINSERT ... ON CONFLICT DO UPDATEper dimension table (cities,districts,streets,postal_codes,addresses); Typesense path posts NDJSON viaTypesenseConnector.ImportDocuments(..., "upsert").
The dataset is country‑wide (~3 M addresses, ~1–2 GB ZIP). Switching backends requires a fresh seed (or RebuildTypesenseIndex to populate Typesense from Postgres).
Dataset metadata (shared across instances)
Each successful seed writes a single-row metadata stamp next to the data itself — a ruian_sync row in PostgreSQL or a ruian_meta collection in Typesense — so multiple Hub instances pointing at the same database see the same state. A Hub that just started up reads the stamp and recognizes that another instance has already populated the dataset; the auto-sync cron therefore does not trigger a redundant import.
AddressesConnector.LoadDatasetMeta($backend) reads the stamp; RUIANSeeder.upsertDatasetMeta(...) writes it after each load. The per-Hub AppSettings.RuianLastSyncAt is kept only as a legacy fallback for GetAddressBackendStatus when the DB metadata is not yet available.
Auto‑sync (cron)
The auto‑sync is a regular Cron task that registers itself on demand. Projects that never use the address autocomplete carry zero cron overhead — the task is added only once both runtime conditions are met:
- Auto‑sync is enabled —
AppSettings.RuianAutoSync === true, persisted in the database (toggled from the admin UI). - A dataset is present in at least one backend —
LoadDatasetMetareturns a non-null stamp from PostgreSQL or Typesense. This is the shared, DB-level signal — it does not matter which Hub instance ran the original seed.
AddressesConnector.RefreshAutoSyncRegistration() evaluates these conditions and adds or removes the "ruian-sync" task accordingly. It is invoked from three places so the registration always reflects the latest state:
Loader.onDbPrepared()— at startup, afterSyncAppSettings, so existing deployments resume the schedule on every restart and Hubs joining a shared database pick up an existing dataset without seeding it again.AddressesConnector.SetAutoSync()— when the admin flips the toggle in the UI.AddressesConnector.LoadAddressesFromRUIAN()— when the first manual seed completes (transitions the system from "no data yet" to "ready to auto-refresh").
The cron expression itself comes from cron.scheduler["ruian-sync"] (default "0 4 * * *" — daily at 04:00; override per project). Each tick calls AddressesConnector.RunAutoSyncTick() which:
- reads the dataset stamp via
LoadDatasetMeta(activeBackend)and returns whenDate.now() - meta.lastSyncAt < intervalHours * 3 600 000(interval not yet elapsed). Because the stamp is shared, a peer instance that just imported counts toward this check too. - returns when the active backend is unreachable (PostgreSQL
SELECT version()/ Typesense/health). - otherwise calls
LoadAddressesFromRUIAN({})which seeds the data and stamps both the DB metadata and the per-HubAppSettings.
Default interval is 720 h (30 days) to match ČÚZK's monthly publication cadence. Schedule and interval are independent: the cron tick fires daily but a real load only happens once the configured number of hours has elapsed.
Key Files
| File | Purpose |
|---|---|
Connectors/AddressesConnector.ts | RPC entry points, dispatcher, ARES integration. |
Connectors/TypesenseConnector.ts | Generic Typesense REST client (singleton). |
Utils/RUIANSeeder.ts | RUIAN orchestrator (Atom resolve → DownloadAsync → UnpackAsync → Expand → CSVHandler.Import → upsert). |
Loader.ts | Calls AddressesConnector.RefreshAutoSyncRegistration() after SyncAppSettings so the cron resumes when conditions are met. |
DAO/Models/AppSettings.ts | Persists the admin's backend choice and the auto‑sync schedule + last-sync stamp. |
Interfaces/IProject.ts | ITypesenseConfiguration, IRuianConfiguration surfaces. |
Interfaces/IRuian.ts, Interfaces/ITypesense.ts | Shared type definitions. |
bin/project/configs/commons.jsonp | Default dbConfigs.typesense and ruian. |
The hub‑gui side mirrors the same set under Connectors/AddressesConnector.ts (RPC stubs + IBackendStatus / IAddressBackendStatus / IRuianLoadOptions / IRuianProgress interface re‑exports), Panels/Dashboard/AddressesPanel.ts (UI) and Controllers/Panels/AddressesPanelController.ts (event wiring).