# 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//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//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= # nume sau institutie form:searchField_input=numePrenume # | "institutia" form:submitButtonSS=cauta javax.faces.ViewState= ``` Response: HTML cu `` rezultate, fiecare rând conține `DownloadServlet?fileName=.pdf&uniqueIdentifier=NTNTARTLNE_`. Pattern fileName: `__.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=` - `data.institutie__regex=` - `data.judet__regex=` - `data.functie__regex=` - `data.tipDeclaratie__regex=` - `data.dataCompletarii__gte=`, `data.dataCompletarii__lte=` - `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 - -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.