--- name: document-register-search description: Search a municipality's or agency's own document register on the Amphora and PDR platforms. module: dokumendiregister.mjs execution: get --- # Document Registers (Amphora and PDR) Every Estonian public body must keep a public register of the documents it sends and receives, so a citizen's letter, a raieluba, a procurement contract and a council decision are all findable in the register of the body that handled them. This guide covers the two platforms outside RIK's ADR: **Amphora** (53 bodies, almost all municipalities) and **PDR** (14 bodies on three shared hosts). For the ADR platform read `ministry-document-registries`; Tallinn and Tartu have guides of their own. **Do not pick the platform yourself.** `authority(name).register` in `asutused.mjs` records both the URL and the platform, and `docRegisterSearch()` reads it from there. Passing an institution's name is the whole route: ```js const r = await docRegisterSearch("Haapsalu Linnavalitsus", "raieluba", { limit: 10 }); show(r); ``` A body whose register is on ADR, Tallinn's or Tartu's platform is refused **by name**, with the function that does serve it named in the error — so a wrong first guess costs one line, not a step. ## Access - **Amphora**: every instance is on ONE host, `https://atp.amphora.ee/`, and the page calls its own JSON API. No session, no token, no POST, no `__VIEWSTATE`. - `GET //api/item?sbrq=&pageSize=4&page=1` — `sbrq` is the register's own global search across every field. - `GET //api/unit` — the bodies this instance hosts, as `org_id` / `org_name`. - `GET //index.aspx?itm=` — one document's page, for a citizen to open. - **PDR**: `https://dok.hm.ee`, `https://adr.envir.ee`, `https://adr.pinal.edu.ee`, each shared by several bodies behind an `orgId`. - `GET /api/v1/documents.json?orgId=&title=&limit=20&offset=0` - `GET /api/v1/options/name/organization.json` — that host's org list, which is what to trust rather than a body's own link. - `GET /et/document.html?id=` — the document's page. `GET /api/v1/documents/.json` is the same record as JSON. - Slugs and orgIds are **not derivable**: Narva-Jõesuu is `njlv`, Antsla is `antslavv2017`. Take them from `authority(name).register.url`, never by guessing. ## Retrieve `docRegisterSearch(body, term, {limit, org})` is the whole interface. It sends only filter names verified to narrow the result, pages safely, and reports the register's own count. - `term` must be at least **3 characters**. Shorter is not refused by either platform — it matches nearly everything and answers HTTP 200, so a one-letter search reads as a result. `sbrq=a` on Rae vald returns 317,899 of its 335,473 documents. - `limit` defaults to 20 and is capped at 60. Amphora returns five rows per request whatever you ask for, so more rows means more round trips against a host that answers in 2–4 s. Narrow the term instead; `totals.reported` already carries the true count. - `org` scopes an Amphora search to one of the bodies on the instance. Take the id from `docRegisterOrgs(body)` — an invented `o2` value takes over a minute to answer and comes back with zero rows. ## Return `{source, page, body, platform, term, ran, totals:{shown, reported, register}, organisations, rows, note}`, each row `{ref, date, title, type, party, url}` with an ISO date and the register's own document URL. - **`totals.reported` is the register's own count over the whole match. State counts from it, never by tallying rows.** - `totals.register` is everything the instance holds, and it is there so the two can be compared — see Verify. - `organisations` names the bodies sharing an Amphora instance. Haapsalu's holds 11: the linnavalitsus plus its schools, kindergartens and culture centre. A count over the instance is therefore **not the municipality's own**, and the module says so in its output when it happens. - `ran: false` means the request was never accepted as a search. That is a broken request, **not** an empty register, and must never be reported as "no such document". ## When the body's own register has nothing A body's own register can be complete, empty or thin — and **the ministry that governs it registers the same correspondence**. Asked what documents existed about Eesti Loodusmuuseumi direktori konkurss, a run searched the museum's 6,414-row register, found nothing about it, and drafted a teabenõue; the five documents were public in Kliimaministeerium's register the whole time. - `parentRegister(name)` in `asutused.mjs` gives that ministry whenever ITS register is searchable, with the exact call for its platform — usually not the same platform as the child's. 42 of the 156 searchable registers have one. - **`reported: 0` raises this as a printed warning by itself. A thin result does not**: the module cannot tell whether five rows about an ideekonkurss answer a question about a direktorikonkurss. That judgement is yours, and it is the same rule. - Only once both registers have been searched may you say the documents are not in the public register, or draft a teabenõue for them. ## Limits - **`pageSize` on Amphora is not a page size.** It is the offset step, and the row count is fixed at five — except the value `5` itself, and omitting it, which return EVERY matching row. On a municipal register that is a multi-megabyte download of the whole register instead of 4 KB. The module sends `pageSize=4`, so consecutive pages overlap by one row and nothing is skipped, and it uses `pageSize=5` only after a bounded request has already told it the result is small. - One instance (Viimsi) runs a rewritten portal: `/api/` answers HTTP 500 and the register is served as HTML. The module falls back to it automatically and returns the same shape, minus `organisations`. The trap there is that the string "Ühtegi kirjet ei leitud" appears in the **hit** page too, inside a JavaScript comment — the count in `` is the signal, not that phrase. ```probe-limit claim One instance (Viimsi) runs a rewritten portal: `/api/` answers HTTP 500 and the register is served as HTML # `/api/item` is the call the module makes on every other instance, and the one # Viimsi answers 500 to. A removed tenant would NOT look like this — an unknown # slug is HTTP 200 with 79 bytes of "Page Not Found" (the bullet below), so the # 500 really is this portal rather than the slug having gone. GET https://atp.amphora.ee/viimsi/api/item?sbrq=leping&pageSize=4&page=1 expect-status 500 expect /An error has occurred/ # The control is the HTML the module falls back to — the second half of the # same sentence. It must answer 200, which is what tells "this instance serves # its register as HTML" from "this instance is down", and those two get the # citizen opposite answers. control GET https://atp.amphora.ee/viimsi/?tab=all&q=leping&p=1 ``` - Amphora answers a slug it does not hold with **HTTP 200 and 79 bytes of `

Page Not Found

` as text/html**. JSON is the acceptance test, not the status code. - Restricted rows are published as registration data only: the title, the date and the number are public, the content is not. Private counterparties appear as initials or not at all. `party` is empty on many municipal instances — that is the register, not a parse failure. - Not covered here: **WebDesktop** (21 bodies) is a different contract entirely — a session cookie carried across three requests — and has its own guide, `webdesktop-document-registers`. `authority(name).register.guide` names the right one; send a WebDesktop register here and nothing works. A handful of registers sit behind a login page or bot detection: those rows are `searchable: false` in the institution table and the honest move is to hand the citizen the link. ## Verify A search only succeeded if `ran` is true. Then compare the two counts the result carries: - `totals.reported` **equal to** `totals.register` means the platform did not recognise the filter and returned the register entire. **Both platforms do this silently, HTTP 200, with a total that looks like a result** — `?sbrqXX=leping` on kbfi reports 265 against the correct 4, and `?titleXX=eelnõu` on dok.hm.ee reports 237,349 against 10,520. The module raises a printed warning when it sees this; do not report such a total as a hit count. - `reported: 0` means the search ran and genuinely matched nothing. Say that; do not retry the same term. - Never name a document without a `url` from its own row. Known-good acceptance checks, run weekly against the live registers by `scripts/check-sources.mjs` (issue #82). Amphora — KBFI is deliberately a small instance, so the counter-example is not a whole-register download: ```probe GET https://atp.amphora.ee/kbfi/api/item?sbrq=leping&pageSize=4&page=1 expect /"total":"[1-9]\d?"/ expect /"item_id":\d+/ # `total` was 4 in 2,080 bytes on 2026-08-02, against 265 documents in the # instance. A misspelt filter name is IGNORED and the whole register comes # back: the counter answers "total":"265" in 3,813 bytes, so it fails the # first assertion while still returning rows. If it ever passes, a search # that was silently dropped has become indistinguishable from a real result. counter GET https://atp.amphora.ee/kbfi/api/item?sbrqXX=leping&pageSize=4&page=1 ``` PDR — Keeleamet, on the shared `dok.hm.ee` host: ```probe GET https://dok.hm.ee/api/v1/documents.json?orgId=148&title=leping&limit=3&offset=0 expect /"recordsFiltered":[1-9]\d{0,3},/ expect /"id":"[0-9a-f-]{36}"/ # 153 of that body's 44,420 documents on 2026-08-02. The same silent-ignore # trap: `titleXX` returns recordsFiltered 44420 — the whole register — with # rows attached, so only the bounded count tells the two apart. counter GET https://dok.hm.ee/api/v1/documents.json?orgId=148&titleXX=leping&limit=3&offset=0 ``` ## Module API docRegisterSearch(body, term, {limit, org}) -> {source, page, body, platform, term, ran, totals, organisations, rows, note}