# ANI Declarații de Avere și Interese — Ingest Plan

**Mission:** ingestăm 1.3M+ declarații PDF ale demnitarilor și înalților funcționari publici din România (2008–2022 + e-DAI 2022→) ca să cross-referențiem **politicieni × firme deținute × contracte SEAP** — flagship feature pentru vreaudigital.ro.

**Cadru legal:** Legea 176/2010 (publicarea declarațiilor e mandate-by-law, GDPR-safe). CNP-ul **nu e public**; tot restul (nume, funcție, instituție, valori, locații imobile, asocieri firme) **este**.

**Status la 2026-05-09:** arhitectură + schemă DB + scraper skeleton. **Full ingest = 15 zile efort focalizat**, nu se face în această sesiune. Acest document e foaia de drum pentru a continua "cold" în următoarea sesiune.

---

## 1. Pipeline (high-level)

```
┌──────────────────────────────────────────────────────────────────────────┐
│  SURSE (3 portaluri ANI, fiecare cu mecanică diferită)                   │
│                                                                          │
│  ▸ old-declaratii.integritate.eu    JSF/IceFaces, search + CSV export    │
│      (2008–2022 archive, ~12M docs, ~1.3M declaratii distincte)          │
│      → /search.html?... POST forms, /DownloadServlet?fileName=…&…        │
│                                                                          │
│  ▸ declaratii.integritate.eu        Angular SPA + Spring Boot REST API   │
│      (e-DAI 2022→, declarații electronice native)                        │
│      → /api/<form-id>/submission JSON cu data.bucket + data.filename     │
│                                                                          │
│  ▸ depozitar.integritate.eu         depozit raw, mirror partial          │
│      (folosit ca fallback dacă portalul principal e down)                │
└────────────────────────────────┬─────────────────────────────────────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────────────────────┐
│  STAGE 1 — Listing scraper (cron/scrape-ani-listings.sh)                 │
│  Walk results pages, populate ani.declaratii (URL + metadata only)       │
│  Idempotent. Dedupe pe (official_name, year, declaration_type, source)   │
│  Output: ~1.3M rows, ~120 MB postgres                                    │
└────────────────────────────────┬─────────────────────────────────────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────────────────────┐
│  STAGE 2 — PDF download (cron/download-ani-pdfs.sh)                      │
│  Fetch PDFs sequential, store on satra disk                              │
│  Path: /opt/vreaudigital-data/ani/{year}/{sha256[:2]}/{sha256}.pdf       │
│  Update ani.declaratii.pdf_path + raw_sha256                             │
│  Estimat: 1.3M × ~300 KB avg = ~400 GB raw                              │
│  Throttled: 2 req/s → ~1 săpt 24/7 sau ~3 săpt @ 8h/zi                  │
└────────────────────────────────┬─────────────────────────────────────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────────────────────┐
│  STAGE 3 — PDF parser (src/parse-ani-pdf.ts)                             │
│  Two pipelines:                                                          │
│   (a) e-DAI (2022→): native text PDFs, generate de Form.io.              │
│       → pdftotext -layout, regex pe câmpuri stabile.                     │
│   (b) Old (2008–2021): scanned + native mix.                             │
│       → pdftotext întâi; dacă < 50 caractere "vizibile" → OCR (tesseract │
│         cu lang=ron, ~5–15s/pagină pe satra).                            │
│  Template-detection: 3 generații de template-uri (2008–2010, 2011–2016,  │
│   2017+). Diferite în text labels dar structuri tabelare comune:         │
│   I. Bunuri imobile, II. Bunuri mobile, III. Active financiare,          │
│   IV. Datorii, V. Donații, VI. Conturi/depozite, VII. Plasamente,        │
│   VIII. Funcții, IX. Asociații/firme deținute, X. Venituri.              │
│  Output: structured rows în ani.bunuri, ani.shareholdings, ani.functii,  │
│   ani.donatii.                                                           │
└────────────────────────────────┬─────────────────────────────────────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────────────────────┐
│  STAGE 4 — Entity resolution                                             │
│  (a) Officials: dedupe across years pe (normalized_name + first          │
│      institution + first year-of-birth slice). CNP-hash neavailable —    │
│      omonimii rezolvate manual prin UI dacă apar conflicte SEAP.         │
│  (b) Shareholdings: parsed firm_name (raw text din PDF) → CUI match      │
│      via firms.match_company_name() (deja deployed în 019_cui_matcher).  │
│      Tier 1: exact name match → 70% acoperire.                           │
│      Tier 2: pg_trgm similarity > 0.8 → +20%.                            │
│      Tier 3: manual review queue → 10% rest.                             │
└────────────────────────────────┬─────────────────────────────────────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────────────────────┐
│  STAGE 5 — UI surfacing                                                  │
│  ▸ /achizitii/politician/[slug]      — profil demnitar (toate            │
│      declaratiile, evolutie netto worth, firme deținute, contracte SEAP) │
│  ▸ /achizitii/firma/[cui]            — adăugăm card "deținută de         │
│      politicianul X" în profilul firmei existente                        │
│  ▸ /achizitii/retete/                                                    │
│       politician-cu-firma-furnizor-stat                                  │
│       (top 50 politicieni a căror firmă a încasat contracte SEAP)        │
│       politician-uat-controleaza-furnizorul                              │
│       (primar/consilier × firma furnizor în UAT-ul lui)                  │
│       evolutie-avere-functie                                             │
│       (politicieni cu cea mai mare creștere netto worth în mandat)       │
└──────────────────────────────────────────────────────────────────────────┘
```

---

## 2. Schema DB (sql/030_ani_schema.sql)

5 tabele, schemă `ani.*`. Toate au `(*)_at` pentru audit + `source_url` ca să fim verifiable.

### `ani.officials` — demnitari/funcționari publici

| col | type | note |
|---|---|---|
| `id` | bigserial PK | |
| `normalized_name` | text NOT NULL | lowercase + unaccent + collapse whitespace |
| `display_name` | text NOT NULL | "Popescu Ioan-Vasile" în casing original |
| `cnp_hash` | char(64) | SHA-256 al CNP dacă l-am extras (RAR — ANI maschează majoritar). Permite linkare across years fără a expune CNP. |
| `first_seen_year` | smallint | min(declaration year) |
| `last_seen_year` | smallint | max(declaration year) |
| `slug` | text UNIQUE | URL-friendly: "popescu-ioan-vasile" + suffix dacă collision |
| `created_at` | timestamptz default now() | |

Index:
- `idx_officials_norm_name` btree pe normalized_name
- `idx_officials_norm_name_trgm` gin pe normalized_name (trgm)
- `idx_officials_slug` unique

### `ani.declaratii` — un PDF = un row

| col | type | note |
|---|---|---|
| `id` | bigserial PK | |
| `official_id` | bigint REFERENCES ani.officials(id) | nullable înainte de Stage 4 entity-resolution |
| `raw_official_name` | text NOT NULL | numele exact cum apare în portal (înainte de normalization) |
| `raw_institution` | text | "Ministerul X" / "Primaria Cluj-Napoca" / "Curtea de Apel Brasov" |
| `raw_function` | text | "Ministru" / "Consilier local" / "Judecator" |
| `raw_localitate` | text | localitatea declarată |
| `raw_judet` | text | județul |
| `year` | smallint NOT NULL | anul declarației (din date completare) |
| `declaration_type` | text NOT NULL CHECK (...) | 'avere' \| 'interese' \| 'avere+interese' |
| `submission_kind` | text | 'anuala' \| 'numire-functie' \| 'incetare-functie' \| 'rectificativa' |
| `data_completare` | date | data completării declarate de demnitar |
| `source_portal` | text NOT NULL | 'old' \| 'new' \| 'depozitar' |
| `source_url` | text NOT NULL | URL public (dacă e old: DownloadServlet…; dacă e new: API submission ID) |
| `source_id` | text | ID intern al portalului (uniqueIdentifier la old, _id la new) |
| `pdf_path` | text | path relativ sub /opt/vreaudigital-data/ani/, NULL până la Stage 2 |
| `pdf_sha256` | char(64) | hash conținut, dedupe |
| `pdf_size_bytes` | integer | |
| `fetched_at` | timestamptz | when PDF was downloaded |
| `parsed_at` | timestamptz | when parser finished |
| `parse_status` | text | 'pending' \| 'ok' \| 'ocr_required' \| 'parse_failed' \| 'template_unknown' |
| `parse_error` | text | last error message |
| `inserted_at` | timestamptz default now() | |

Index:
- `idx_declaratii_official` (official_id, year DESC) WHERE official_id IS NOT NULL
- `idx_declaratii_year` (year DESC, declaration_type)
- `idx_declaratii_sha` UNIQUE (pdf_sha256) WHERE pdf_sha256 IS NOT NULL
- `idx_declaratii_source` UNIQUE (source_portal, source_id)
- `idx_declaratii_pending` (parse_status) WHERE parse_status IN ('pending','ocr_required')
- `idx_declaratii_raw_name_trgm` gin pe raw_official_name

### `ani.bunuri` — secțiunile I (imobile) + II (mobile)

| col | type | note |
|---|---|---|
| `id` | bigserial PK | |
| `declaration_id` | bigint NOT NULL REFERENCES ani.declaratii(id) ON DELETE CASCADE | |
| `category` | text NOT NULL CHECK (...) | 'imobil-teren' \| 'imobil-cladire' \| 'mobil-vehicul' \| 'mobil-bijuterii' \| 'mobil-altele' |
| `subcategory` | text | "agricol" / "intravilan" / "apartament" / "casa" / "auto" |
| `localitate` | text | judet/țara/localitate text |
| `judet` | text | județ-normalizat unde aplicabil |
| `tara` | text | implicit "România" |
| `year_acquired` | smallint | anul dobândirii |
| `mode_acquired` | text | "cumparare" \| "mostenire" \| "donatie" \| "constructie" |
| `area_sqm` | numeric | suprafață în m² (terenuri/clădiri) |
| `share_pct` | numeric | cota-parte (1.0 = integrală) |
| `co_owner` | text | numele co-proprietarului dacă declarat |
| `value_lei` | numeric | valoarea declarată |
| `value_currency` | text default 'RON' | uneori EUR/USD |
| `raw_row_text` | text | textul brut din PDF, ca audit trail |

Index: `idx_bunuri_decl` (declaration_id), `idx_bunuri_judet` (judet) WHERE judet IS NOT NULL.

### `ani.shareholdings` — secțiunea IX (firme deținute) + secțiunea VIII partial (asociat) — **flagship table**

| col | type | note |
|---|---|---|
| `id` | bigserial PK | |
| `declaration_id` | bigint NOT NULL REFERENCES ani.declaratii(id) ON DELETE CASCADE | |
| `firm_name_raw` | text NOT NULL | textul brut din PDF |
| `firm_cui` | text | rezolvat în Stage 4, NULL în primă fază |
| `firm_match_score` | real | similarity la match |
| `firm_match_method` | text | 'exact_name' \| 'trgm' \| 'manual' \| 'unmatched' |
| `role` | text | "actionar" \| "asociat" \| "membru CA" \| "administrator" \| "cenzor" \| "membru AGA" |
| `share_pct` | numeric | cota deținută (dacă declarată) |
| `value_lei` | numeric | valoarea participațiunii |
| `category` | text | 'societate' \| 'asociatie' \| 'fundatie' \| 'cooperativa' \| 'altele' |
| `raw_row_text` | text | audit |

Index:
- `idx_share_decl` (declaration_id)
- `idx_share_cui` (firm_cui) WHERE firm_cui IS NOT NULL
- `idx_share_name_trgm` gin pe firm_name_raw

### `ani.functii` — secțiunea VIII (funcții deținute, public + privat)

| col | type | note |
|---|---|---|
| `id` | bigserial PK | |
| `declaration_id` | bigint NOT NULL REFERENCES ani.declaratii(id) ON DELETE CASCADE | |
| `is_public` | boolean | TRUE = funcție în instituție publică |
| `function_name` | text NOT NULL | "Consilier", "Ministru", "Director general" |
| `institution_name` | text NOT NULL | numele instituției / firmei |
| `institution_cui` | text | rezolvat în Stage 4 (joinable cu firms.entities sau seap.cui_authority) |
| `start_year` | smallint | |
| `end_year` | smallint | NULL dacă activă |
| `salary_lei` | numeric | venit anual din această funcție (când declarat) |
| `raw_row_text` | text | |

Index: `idx_functii_decl` (declaration_id), `idx_functii_inst_cui` (institution_cui) WHERE institution_cui IS NOT NULL.

### `ani.donatii` — secțiunea V (donații primite)

| col | type | note |
|---|---|---|
| `id` | bigserial PK | |
| `declaration_id` | bigint NOT NULL REFERENCES ani.declaratii(id) ON DELETE CASCADE | |
| `donor_name` | text | cine a făcut donația |
| `donation_type` | text | 'bani' \| 'imobil' \| 'mobil' \| 'servicii' |
| `value_lei` | numeric | |
| `currency` | text default 'RON' | |
| `year_received` | smallint | |
| `raw_row_text` | text | |

Index: `idx_donatii_decl` (declaration_id).

---

## 3. Estimări de volum

| Stage | Estimat | Notă |
|---|---|---|
| officials (distinct) | ~150K | demnitari + magistrati + înalți funcționari activi în 2008–2025 |
| declaratii (rows) | ~1.3M | 8–10 declarații/persoană în medie pe carieră |
| pdf storage | ~400 GB | 300 KB avg × 1.3M |
| bunuri (rows) | ~6M | 4–5 bunuri/declarație medie |
| shareholdings (rows) | ~800K | doar 30–40% au firme declarate |
| functii (rows) | ~3M | 2–3 funcții/declarație |
| donatii (rows) | ~250K | rare (10–20% au donații) |

**DB size estimat:** ~12 GB (fără PDF-uri, doar metadata + parsed).

**Cross-source magic queries posibile după ingest:**
1. `ani.shareholdings JOIN firms.entities ON cui JOIN seap.announcements ON supplier_cui` → politicianul X are firma Y care a câștigat 50M lei contracte.
2. `ani.functii(institutie publica) JOIN seap.announcements(authority) ON cui` → consilier local × autoritatea unde votează.
3. Year-over-year diff pe `ani.declaratii.bunuri` → creștere bruscă de avere în mandat.

---

## 4. Plan de execuție 15 zile

### Faza 1 — Listing & metadata (Days 1–2)
- **Day 1:** Scraper pentru **old portal** (JSF/IceFaces). Reverse-engineer formul `/search.html` cu pagination prin "Cautare avansata" + date range slicing (lună de lună 2008–2022 ca să nu lovim limita de rezultate). Output: ani.declaratii cu source_url + metadata, fără PDF-uri. Test: 1000 rows pe februarie 2020.
- **Day 2:** Scraper pentru **new portal** (Angular SPA → Spring Boot REST). Reverse-engineer endpoint-ul `/api/<form-id>/submission` cu request real captured din DevTools (TODO: necesită browser session pentru a observa traffic). Test: 100 rows e-DAI 2024.

**Deliverable Day 2:** ~50K rows în ani.declaratii (sample), 0 PDF-uri downloaded.

### Faza 2 — PDF download (Days 3–4)
- **Day 3:** `cron/download-ani-pdfs.sh` cu rate limit 2 req/s + retry exponential. Storage la `/opt/vreaudigital-data/ani/{yyyy}/{sha256[:2]}/{sha256}.pdf`. Update declaratii.pdf_path + sha + size + fetched_at. Run pe 1000 PDFs pilot.
- **Day 4:** Scale-up. Background detached docker container, log la `/var/log/vreaudigital-ani-pdfs.log`. Lasă să meargă în paralel cu munca pe parser.

### Faza 3 — Parser PDF (Days 5–7)
- **Day 5:** Setup `pdftotext` în container + helper Node `src/parse-ani-pdf.ts`. Detect template (2008-2010 / 2011-2016 / 2017+ / e-DAI). Parse secțiunea I (imobile) ca proof-of-concept. Test pe 10 PDFs din fiecare era.
- **Day 6:** Parser secțiunile II (mobile) + IX (shareholdings). Acestea sunt cheia. Output în ani.bunuri și ani.shareholdings. Test pe 100 PDFs.
- **Day 7:** Secțiunile VIII (functii) + V (donatii). OCR fallback (tesseract ron) pentru PDF-uri scanate (estimat 15-25% din 2008-2014). Marcăm `parse_status='ocr_required'` și rulăm OCR într-un cron separat.

**Deliverable Day 7:** parser care procesează ~70% din PDF-uri auto, ~25% cu OCR, ~5% template-unknown (manual review).

### Faza 4 — Entity resolution (Days 8–10)
- **Day 8:** Officials dedup. SQL function `ani.dedup_officials()` care grupează ani.declaratii pe (normalized_name + raw_judet + first-year). Manual review pentru top 1000 ambiguous (UI viewer simplu).
- **Day 9:** CUI matching pentru shareholdings. Refolosim `firms.match_company_name()` din 019_cui_matcher. Tier 1 exact + Tier 2 trgm > 0.8. Restul → tabel ani.shareholdings_unmatched_queue pentru review.
- **Day 10:** CUI matching pentru functii.institution_cui. Authority-side: lookup în `seap.cui_authority`. Private-side: lookup în firms.entities.

**Deliverable Day 10:** ~85% din shareholdings au CUI rezolvat → joinable cu seap și firms.

### Faza 5 — UI (Days 11–13)
- **Day 11:** `/achizitii/politician/[slug]` — pagină profil. Cards: declarații (timeline), evoluție avere, top firme deținute, contracte câștigate de firmele lui prin SEAP. Endpoint API la `/api/politician/[slug]`.
- **Day 12:** Cross-link în pagina existentă `/achizitii/firma/[cui]`: section "Asociat cu politicieni (declarații ANI)" — list de officials cu link la profil.
- **Day 13:** Recipe page `politician-cu-firma-furnizor-stat`. Top 50 politicieni unde COALESCE(firma.contracte_seap_total) > 0. Plus 2 recipe variants (evoluție avere, primar × furnizor UAT).

### Faza 6 — Polish (Days 14–15)
- **Day 14:** Materialized views pentru perf: `mv_official_seap_exposure` (politician → total contracte SEAP firme proprii), refresh nightly. Indexes finali. Analyze.
- **Day 15:** Testing. Edge cases: persoane omonime (Popescu Ion × 50), firme cu nume identice cu funcții ("ASOCIAȚIA"), declarații fără PDF rezolvabil, OCR errors (CUI = "S.C. SRL" → garbage). Documentare pentru următoarea sesiune. Disclaimer GDPR în pagina /despre.

---

## 5. Risk register

| Risc | Probabilitate | Impact | Mitigare |
|---|---|---|---|
| Anti-scraping (rate limits, IP block) | Medie | Mare | User-Agent identifier ("gov-agreg/1.0 vreaudigital.ro contact:..."), 2 req/s max, retry exponential, fallback la depozitar.integritate.eu. ANI nu are istoric de a bloca scraperi (briatte/integritate a mers fără probleme 2017-2019). |
| PDF template change mid-corpus | Mare | Medie | Detector explicit per-template (regex pe header text); marker `parse_status='template_unknown'` pentru manual review. Quarterly check. |
| OCR errors → CUI invalid | Mare | Medie | Validare CUI cu checksum oficial (algoritm pe ultima cifră). Multe vor pica; tier 3 manual queue. |
| Name disambiguation (omonimii) | Mare | Mare | Default conservativ: NU merge officials cu nume identic dacă funcție/judeţ diferă. UI marker "posibil aceeași persoană" cu disclaimer. |
| GDPR challenges | Mică | Mare | Tot ce publicăm are basis legal (Legea 176/2010). Disclaimer prominent. NIMIC din CNP/data nașterii nu apare în UI. Privacy policy explicit. Right-to-rectify accesibil prin /contact. |
| Old portal sunset | Mare (anunțat 2025) | Mare | **Prioritate:** ingestăm rapid old portal înainte de takedown. Cache local PDF-uri ca single source of truth. New portal e SPA fragil → backup. |
| Volum PDF (400 GB) | Medie | Medie | Storage pe satra: avem ~2 TB free. Compress PDFs (zstd -19) la cold storage după parsing → ~120 GB. |
| Effort > 15 zile | Mare | Medie | MVP shippable la Day 13 (UI + recipe), zilele 14-15 sunt polish. Faza 4 (entity resolution) e cea mai imprevizibilă; dacă pică, ship cu shareholdings unmatched + UI care arată "candidat firmă declarată: X (nu am putut face matching automat)". |

---

## 6. Decizii de arhitectură (locked-in)

1. **Storage PDFs:** filesystem pe satra, NU în Postgres bytea. Path templated pe sha256. Permite rsync/backup separat.
2. **Officials sunt dedupliated DUPĂ ce avem PDFs parsed**, nu înainte. ani.declaratii.official_id e nullable înainte de Stage 4.
3. **CNP nu se stochează în clear**, doar hash dacă e parsed (rar — ANI maschează în majoritatea cazurilor). Folosim doar pentru disambiguation, nu pentru afișare.
4. **Două scrapers separate** (old + new), nu unul unificat. Mecanicile sunt prea diferite (JSF vs REST). Schema DB unificată via source_portal column.
5. **Parser e batch**, nu online. Rulează nightly via cron. Nu blocăm scraper-ul de listing.
6. **Recipe registration:** slot `politician-cu-firma-furnizor-stat` adăugat acum în RECIPES (returnează empty rows până avem date) — keeps URL stabil pentru SEO și menționabil în comunicare publică ("vine în curând").

---

## 7. Open questions (de rezolvat în sesiune următoare)

1. **e-DAI API endpoint exact:** trebuie capturat din DevTools într-o sesiune browser reală (Selenium / Playwright). Bundle-ul SPA îl construiește runtime din config necunoscut. Plan: rulăm un browser headless 1x să capturăm 2-3 cereri și să reverse-engineerăm.
2. **Old portal CSV export:** există un buton "Exporta resultate" — dacă funcționează prin POST simplu, sărim peste paginare HTML și luăm CSV bulk. Trebuie verificat manual.
3. **Tesseract pe satra:** confirma că modelul `ron` e instalat. Estimat 5-15s/pagină pe CPU; pentru 200K PDFs OCR-required = 2-3 zile la concurrency 8.
4. **Slug uniqueness pentru politicieni cu nume identice:** Popescu Ion poate fi 50 de oameni. Strategy: `nume-prenume-judet-functie-prima-aparitie`? Vezi după dedupe.

---

## 8. API endpoints discovered (live verification 2026-05-09)

### Old portal (PRIMARY ingestion target)

`https://old-declaratii.integritate.eu/search.html`

JSF/IceFaces, POST cu form data:
```
form=form
form:searchKey_input=<query>            # nume sau institutie
form:searchField_input=numePrenume      # | "institutia"
form:submitButtonSS=cauta
javax.faces.ViewState=<grabbed-from-GET-search.html>
```
Response: HTML cu `<table>` rezultate, fiecare rând conține `DownloadServlet?fileName=<X>.pdf&uniqueIdentifier=NTNTARTLNE_<NUM>`.

Pattern fileName: `<unique_id>_<persona_id>_<seq><suffix>.pdf` unde suffix `_a` = avere, `_b` = interese (probabil; de validat pe corpus mai mare).

Coloane în tabel: Nume Prenume / Institutie / Functie / Localitate / Judet / Data completare / Tip declaratie / Vezi declaratie / Distribuie.

Pagination: `form:resultsTable_pageInput`, `form:resultsTable_pageButton` — JSF AJAX. Soluție: date range slicing (lună de lună) ca să nu lovim limita de pagini.

**No auth, no captcha, no rate limit explicit.** Confirmed working 2026-05-09.

### New portal (e-DAI 2022→) — captcha protected

`https://depozitar.integritate.eu/api/formio/grid/documente/submission`

JSON REST API. Filtre cunoscute (Form.io syntax):
- `data.numePrenume__regex=<text>`
- `data.institutie__regex=<text>`
- `data.judet__regex=<JUDET-uppercase>`
- `data.functie__regex=<text>`
- `data.tipDeclaratie__regex=<text>`
- `data.dataCompletarii__gte=<ISO>`, `data.dataCompletarii__lte=<ISO>`
- `data.show__regex=1` (filtru de bază pentru declarații publicate)
- `sort=-created`, `limit=N`, `skip=N`

**Returnează 401 fără token Cloudflare Turnstile** (`x-jwt-token` header). Necesită browser headless (Playwright) sau solver. Punem în Phase 2 zile 8-9 dacă merită.

Per-document: `data.bucket` + `data.filename` → API download via separate endpoint (TBD, capturat din browser session).

### Depozitar.integritate.eu

Mirror al new portal-ului, aceeași API + Turnstile. Folosit ca fallback când portalul principal e down.

## 9. Sample PDFs analizate (Task 2)

5 PDFs descărcate de pe old-declaratii (stocate în `satra:/tmp/ani_samples/`):

| # | Persoană | An | Tip | Producer | Pages | Bytes | OCR? |
|---|---|---|---|---|---|---|---|
| 1 | KLAUS WERNER IOHANNIS (Președintele României) | 2024 | avere | iText 5.5.13.2 | ~5 | 60 KB | nu |
| 2 | KLAUS WERNER IOHANNIS | 2017 | avere | iText (similar) | ~5 | 60 KB | nu |
| 3 | KLAUS WERNER IOHANNIS | 2014 | avere | (no producer) | scanned | 293 KB | **DA** |
| 4 | EMIL BOCA (politist penitenciare Gherla — homonim cu Boc) | 2024 | avere | Kodak Capture | scanned | 58 KB | **DA** |
| 5 | CATALIN PREDOIU (Vice prim-ministru) | 2024 | avere | Alaris Capture | scanned | 112 KB | **DA** |

### Observații cheie

1. **Native vs scanned NU e funcție de an** — depinde de cum a încărcat funcționarul. Iohannis 2024 e iText nativ (probabil generat din formular electronic intern), Predoiu 2024 e Alaris scanat (a printat → semnat → scanat). În practică: ~30-50% din PDF-uri necesită OCR independent de an.

2. **Toate PDF-urile native au structură IDENTICĂ** — același template iText cu secțiunile I-X marcate cu litere romane. Layout tabular cu 6-7 coloane pentru fiecare secțiune. `pdftotext -layout` păstrează structura suficient cât regexes per-section funcționează.

3. **CNP e mascat în native PDFs** (`*************`) → nu vom putea extrage CNP-uri pentru disambiguation. Ne bazăm pe `(name + institutie + judet + first_year)`.

4. **Localitate / Adresa sunt parțial mascate** (`***********`) pentru proprietăți → confirmă conformitatea ANI cu GDPR (adresă completă nu e public). Avem judeţul. Suficient pentru cross-check.

5. **Sample text extras** (Iohannis 2024 secțiune I.2 Clădiri):
   ```
   Tara: ROMANIA
   Judet: Sibiu
   Localitate: Sibiu
   Adresa: ***********
   Categorie: Apartament
   Anul dobândirii: 1997
   Suprafata: 84.60 m2
   Cota-parte: 1/1
   Modul de dobândire: Contract de vânzare cumpărare
   Titularul: IOHANNIS CARMEN, IOHANNIS KLAUS
   ```
   → toate câmpurile pentru `ani.bunuri` rezolvabile cu regex per-tabel.

6. **Filename suffix decoding (preliminary):**
   - `_a.pdf` la sfârșit → declarație avere (confirmed pe Iohannis 2024 + 2017)
   - `_b.pdf` la sfârșit → declarație interese (de validat)
   - `_NNN.pdf` (3 cifre) la sfârșit → variantă numerotată (rectificative? batch upload?)

### Recomandare parser

**Strategie pe 3 nivele:**

1. **Tier 1: pdftotext -layout + regex per-secțiune** (rapid, ~50 ms/PDF). Se aplică tuturor PDFs.
   - Dacă output > 500 chars vizibili (nu doar headere) → procesăm.
   - Folosim markeri "I. Bunuri imobile", "II. Bunuri mobile", "VIII. ", "IX. " ca anchor pentru extragerea blocurilor de text.

2. **Tier 2: detect scanned + OCR** (lent, ~5-15s/PDF). Aplicat când Tier 1 returnează < 500 chars.
   - `tesseract <pdf-img> - -l ron` în container. PDF → imagine via `pdftoppm -r 200` întâi.
   - Output mai zgomotos: regex relaxat, mai mulți falși pozitivi.

3. **Tier 3: template_unknown** (ratele PDF-uri parsate nu match niciun template). Coadă manuală review în UI admin.

**Tools:**
- **pdftotext (poppler-utils)** + Node.js — nu Python (`pdfplumber` ar fi mai elegant dar adaugă dependency Python într-un repo TS).
- **tesseract-ocr-ron** — în container alpine cu `apk add tesseract-ocr tesseract-ocr-data-ron`. Estimat 5-15s/PDF pe satra CPU. 200K PDFs scanate × 8s = 18 zile single-thread → cu concurrency 8 = ~3 zile.
- **NO Apache Tika** — overkill, mai bine pdftotext direct.

**Effort:** parser MVP la 70% acuratețe e ~3 zile (Day 5-7). Restul de 30% (template-uri vechi 2008-2010, edge cases) ajunge la 90% în următoarea iterație.

