+
    >pj_D                     2   R t ^ RIt^ RIt^ RIHt ^ RIHt ^ RIHt ^ RI	H
t
 ^ RIHt ^ RIHt ^ RIHtHt ^ R	IHt ^ R
IHt ^ RIHtHt ]P2                  ! ]4      tRtR R ltR R ltR R ltR R lt R R lt!R R lt"RR R llt#RRRR/R R llt$R# ) u  Arquivo nacional da Distribuição do CITIUS: ingestão e cruzamento.

A varredura sempre descarregou o país inteiro — a consulta é feita sem filtro de
parte — mas guardava só o que dava match e deitava o resto fora em memória. Como
a fonte só serve ~180 dias, isso era informação a perder-se todos os dias.

Aqui a ordem inverte-se: **primeiro guarda-se, depois cruza-se**. É a mesma forma
do `cire_announcement_ingest` + `insolvency_matching`, e a razão é a mesma: um
cruzamento que vive à parte pode voltar a correr. Quando entra um cliente novo,
ou quando a regra melhora, reaplica-se ao arquivo — em vez de valer só daí para
a frente, que é o que acontece hoje com uma correspondência falhada.

**Dois critérios, com pesos diferentes.** O NIF é o bom: o `insolvency_matching`
já escolheu só esse porque um falso positivo custa caro — alguém vai agir
convencido de que um cliente está em tribunal. Mas a Distribuição não publica
NIF, só nomes. O que o arquivo permite é ir buscá-lo ao grafo do registo
comercial, que tem 1,27 milhões de entidades com nome e NIF. O que não resolver
fica com o critério antigo, o distintivo, marcado como tal no `match_kind` para
se poder rever depois só o que é frágil.
N)date)Any)fuzz)text)AsyncSession)settings)build_distintivos_mapmatch_row_to_companies)filing_hash)upsert_process)normalize_nameparse_moneyi  c                    V ^8  d   QhR\         R\        \        \        3,          R\        \        \
        3,          /# )   sessionrowreturn)r   dictstrr   tuplebool)formats   "app/services/court_filings.py__annotate__r   ,   s8     F( F( F(DcN F(uSRVYGW F(    c                   "   VP                  R4      p\        VP                  R4      VP                  R4      V4      p\        VP                  R4      4      w  rEVP                  4        UUu/ uF  w  rgVR9  g   K  WgbK  	  pppT P	                  \        R4      RVP                  R4      ;'       g    RP                  4       RVP                  R4      ;'       g    RP                  4       R	VP                  R	4      R
VP                  R
4      RVP                  R4      RVRVRVP                  R4      RVP                  R4      R\        P                  ! VR\        R7      RV/4      G Rj  xL
 p	V	P                  4       w  rV'       d   VP                  R4      ;'       g    .  F  pVP                  R4      ;'       g    RP                  4       pV'       g   K6  T P	                  \        R4      RT
RVR,          R\        V4      R,          RVP                  R4      ;'       g    RP                  4       R,          ;'       g    R/4      G Rj  xL
  K  	  \        V
4      \        V4      3# u uppi  EL
 L'5i)u4  Grava uma linha da Distribuição e as suas partes. Devolve (id, é_nova).

O `raw` **não leva as partes** — elas têm tabela própria. É a lição que o
`processes.raw` deu ao duplicá-las e custar 7,7 kB por linha; a esta escala,
~230 mil processos por ano a 6 partes cada, seriam ~80 MB/ano por nada.
data_distribuicao_dprocess_numbertribunalvalorpartiesdata_entrada_du  
            INSERT INTO court_filings (
                process_number, tribunal, unorganica, especie, valor_raw, valor,
                data_distribuicao, data_entrada, observacoes, raw, dedup_hash
            ) VALUES (
                :process_number, :tribunal, :unorganica, :especie, :valor_raw, :valor,
                :data_distribuicao, :data_entrada, :observacoes, CAST(:raw AS JSONB), :hash
            )
            ON CONFLICT (dedup_hash) DO UPDATE SET
                last_seen_at = now(),
                -- Só se enriquece: uma re-observação não apaga o que já se sabia.
                unorganica = COALESCE(EXCLUDED.unorganica, court_filings.unorganica),
                especie = COALESCE(EXCLUDED.especie, court_filings.especie),
                valor = COALESCE(EXCLUDED.valor, court_filings.valor),
                observacoes = COALESCE(EXCLUDED.observacoes, court_filings.observacoes)
            RETURNING id::text, (xmax = 0) AS is_new
             
unorganicaespecie	valor_rawdata_distribuicaodata_entradaobservacoesrawFensure_asciidefaulthashNnamez
                    INSERT INTO court_filing_parties (filing_id, name, name_norm, role)
                    VALUES (:fid, :name, :norm, :role)
                    fid:Ni  Nnormrole:Nd   N)r    r   r!   )getr
   r   itemsexecuter   stripjsondumpsr   firstr   r   )r   r   	data_disthr   _moedakvr)   result	filing_idis_newpr.   s   &&            r   upsert_filingrC   ,   s3     -.ICGG,-swwz/BINA 01MEIIK
iKDA14h+h414KC
i??	
( sww'78>>BEEG,2299;#'',/swwy))UCGG$45377=14::csCA	
)! !FD I#))r)AEE&M''R..0D// 9DJ N406QUU6]00b779$?GG4  	 ** y>4<''y j!Rsb   A,I>.I3>I30I>5(I>BI>/I905I>&I>I>AI>*I>	I>I<%I><I>c                R    V ^8  d   QhR\         R\        \        ,          R\        /# r   r   
filing_idsr   r   listr   int)r   s   "r   r   r   u   s%     *  * l * S	 * c * r   c                   "   V P                  \        R4      RV/4      G Rj  xL
 pVP                  ;'       g    ^ #  L5i)u  Dá NIF às partes que o grafo do registo consegue identificar sem dúvida.

**Só entidades colectivas.** Uma pessoa singular com nome raro podia ser
única no registo e ainda assim não ser a pessoa que está em tribunal — o
nome não chega para identificar gente, e é a mesma razão pela qual o
`is_company_party` do scraper exige forma societária.

**E só quando não há ambiguidade.** Se dois NIPCs partilham o nome
normalizado, não há como escolher e fica por resolver: preferimos falhar um
match a inventar um.
a  
            WITH alvo AS (
                SELECT DISTINCT p.name_norm
                  FROM court_filing_parties p
                 WHERE p.nif IS NULL
                   AND p.name_norm <> ''
                   AND p.filing_id = ANY(CAST(:ids AS UUID[]))
            ),
            resolvidas AS (
                SELECT a.name_norm, min(e.nif) AS nif
                  FROM alvo a
                  JOIN registry_entities e ON e.name_norm = a.name_norm
                 WHERE e.nif IS NOT NULL
                   AND e.merged_into IS NULL
                   AND e.kind IN ('company', 'public', 'foreign')
                 GROUP BY a.name_norm
                HAVING count(*) = 1
            )
            UPDATE court_filing_parties p
               SET nif = r.nif, nif_source = 'grafo'
              FROM resolvidas r
             WHERE p.name_norm = r.name_norm
               AND p.nif IS NULL
               AND p.filing_id = ANY(CAST(:ids AS UUID[]))
            idsN)r5   r   rowcount)r   rF   r?   s   && r   resolve_party_nifsrM   u   sJ      ??	
6 

9 F< ??a=s   !A >A A c                h    V ^8  d   QhR\         R\        \        ,          R\        \        ,          /# rE   )r   rH   r   )r   s   "r   r   r      s)          49  c  r   c                   "   V P                  \        R4      RV/4      G Rj  xL
 P                  4       pV Uu. uF  q3^ ,          NK  	  up#  L*u upi 5i)z=Acertos pelo NIF resolvido. Devolve os ids dos acertos novos.a  
                INSERT INTO court_filing_hits (
                    filing_id, party_id, company_id, watchlist_id,
                    matched_nif, matched_name, matched_role, match_kind, score
                )
                SELECT p.filing_id, p.id, c.id, w.id, p.nif,
                       COALESCE(c.legal_name, w.name, p.name), p.role, 'nif', 1.0
                  FROM court_filing_parties p
                  LEFT JOIN companies c ON c.nif = p.nif
                  LEFT JOIN insolvency_watchlist w ON w.nif = p.nif AND w.active
                 WHERE p.filing_id = ANY(CAST(:ids AS UUID[]))
                   AND p.nif IS NOT NULL
                   AND (c.id IS NOT NULL OR w.id IS NOT NULL)
                ON CONFLICT DO NOTHING
                RETURNING id::text
                rK   N)r5   r   all)r   rF   rowsrs   &&  r   _hits_por_nifrS      sb      oo$ J'
 	
* 
ce- 	. $QaDD$-	
,  s    !AAAAAAc          
          V ^8  d   QhR\         R\        \        ,          R\        \        \        \        \        3,          3,          R\        \        ,          /# )r   r   rF   distintivosr   )r   rH   r   r   r   )r   s   "r   r   r      sM     U UU'+CyU?CCcSVhDW?XU	#YUr   c                  "   V P                  \        R4      RV/4      G Rj  xL
 P                  4       p/ pV F0  w  rVrxp	VP                  V. 4      P	                  RVRVRVRV	/4       K2  	  . p
VP                  4        EF  w  r[\        RV/V4       EF  w  rVP                  R4      ;'       g    R	P                  4       pVP                  R4      ;'       g    R	P                  4       pV'       d	   W8w  d   Kh  \        P                  ! VP                  R4      ;'       g    R	P                  4       VP                  R
4      ;'       g    R	P                  4       4      R,          pV\        P                  8  d   K  T P                  \        R4      RTRVR,          RVR,          RVP                  R
4      ;'       g    VP                  R4      RVP                  R4      R\        V^4      /4      G Rj  xL
 P                  4       pV'       g   EK  V
P	                  V^ ,          4       EK  	  EK  	  V
#  EL LD5i)u  Acertos pelo nome, para o que o NIF não resolveu — com duas guardas.

O `match_row_to_companies` compara o **prefixo** de duas palavras. Sozinho,
isso atribuiu 316 processos e, das 119 vezes em que houve NIF para conferir,
**errou 119** — nem uma acertou. "Centro de Jardinagem Vila Jasmim" ia parar
ao Centro de Cultura e Desporto de Ronfe, e os processos da Prosegur
Logística, da Prosegur Alarmes e até da Prosegur *Seguros* eram todos
carimbados à Prosegur - Companhia de Segurança.

Por isso o prefixo passa a ser só o primeiro crivo, seguido de dois:

1. **O NIF manda, quando existe.** Se a parte foi resolvida para um NIF e ele
   não é o da empresa, a correspondência está provada errada. Não é uma
   suspeita, é uma contradição.
2. **O nome inteiro tem de parecer-se**, não só o princípio — o mesmo
   `token_set_ratio` a `FUZZY_MATCH_THRESHOLD` que o caminho por empresa já
   usava. É o que separa "Prosegur - Companhia de Segurança, Lda." de
   "Prosegur - Logística e Tratamento de Valores".
z
                SELECT p.filing_id::text, p.id::text, p.name, p.role, p.nif
                  FROM court_filing_parties p
                 WHERE p.filing_id = ANY(CAST(:ids AS UUID[]))
                 ORDER BY p.filing_id
                rK   Nidr.   r1   nifr    r"   
legal_nameg      Y@a   
                        INSERT INTO court_filing_hits (
                            filing_id, party_id, company_id,
                            matched_name, matched_role, match_kind, score
                        ) VALUES (
                            CAST(:fid AS UUID), CAST(:pid AS UUID), CAST(:cid AS UUID),
                            :name, :role, 'nome', :score
                        )
                        ON CONFLICT DO NOTHING
                        RETURNING id::text
                        r/   pidcidscore)r5   r   rP   
setdefaultappendr4   r	   r3   r6   r   token_set_ratiolowerr   FUZZY_MATCH_THRESHOLDroundr9   )r   rF   rU   partespor_processor@   party_idr.   r1   rX   novosr    companyparty	nif_partenif_empresar\   r   s   &&&               r   _hits_por_nomerk      s    . oo J

 
	
 
ce  57L06,	T	2.558VT64D	
 17
 E*002	4i5I;WNG5)//R668I";;u-33::<KY5$$YYv&,,"335[[.44";;= 	  x555oo
 yuT{wt}L 9 N NUYYv=N		& 1ua . eg1 2 sSV$Q X 3T L}
	
HsM   !H?H:BH?2(H?H?4/H?$'H?A4H?7H?8H=9H?%H?=H?c                R    V ^8  d   QhR\         R\        \        ,          R\        /# )r   r   hit_idsr   rG   )r   s   "r   r   r     s%     : :| :d3i :C :r   c                r  "   V'       g   ^ # V P                  \        R4      RV/4      G Rj  xL
 P                  4       p^ pV F  w
  rErgrrrV P                  \        R4      RV/4      G Rj  xL
 P                  4       p\        T;'       g    / 4      pV UUUu. uF  w  pppRVRVRV/NK  	  upppVR	&   \	        T R
TRRRTRTRTRT	RT
RT;'       g    TR	VR	,          R\
        P                  ! VR\        R7      RR/4      G Rj  xL
 w  ppTV'       d   ^M^ ,          pK  	  V#  L Lu upppi  L+5i)uS  Escreve na `processes` o que os acertos novos trouxeram.

A `processes` continua a ser exactamente o que era — a vista das empresas
monitorizadas — e continua a ser escrita pelo `upsert_process`, com a sua
lista negra de falsos positivos e os seus eventos. Só mudou de onde vem a
linha: do arquivo, em vez de directamente do scraper.
a  
                SELECT h.company_id::text, f.id::text, f.process_number, f.tribunal,
                       f.unorganica, f.especie, h.matched_role, f.data_distribuicao,
                       f.data_entrada, f.raw
                  FROM court_filing_hits h
                  JOIN court_filings f ON f.id = h.filing_id
                 WHERE h.id = ANY(CAST(:ids AS UUID[]))
                   AND h.company_id IS NOT NULL
                rK   NzUSELECT name, role, nif FROM court_filing_parties WHERE filing_id = CAST(:fid AS UUID)r/   r.   r1   rX   r    
company_idsourcedistribuicaor   r   juizospeciesrole_in_process
date_filedraw_jsonFr*   raw_html)r5   r   rP   r   r   r7   r8   r   )r   rm   linhasescritosro   r@   numeror   r#   r$   papelr:   data_entr)   rc   entrynrR   rX   _pidrA   s   &&                   r   _processos_dos_hitsr     sp     oo
 G
 	
 
ce " H-3	**	8//< 	"  #% 	 SYYBPVWPV91aVQ5#>PVWi+j. &H7!5i3385+DJJu5#ND
 
f  	AQ&9 .4: O_	
( X
sQ   +D7D*?D7-D,.D7D7D.
0%D73D7	D5
D7D7,D7.D7c                t    V ^8  d   QhR\         R\        \        \        \        \        3,          3,          /# )r   r   r   )r   r   r   r   )r   s   "r   r   r   S  s,     > > >c4S>>Q9R >r   c                   "   V P                  \        R4      4      G Rj  xL
 P                  4       P                  4       p\	        V Uu. uF  p\        V4      NK  	  up4      #  LDu upi 5i)u<  O mapa de distintivos das empresas activas.

Fica à parte para se construir **uma vez por corrida**, não uma vez por lote:
o `build_distintivos_map` avisa a cada nome ambíguo que encontra ("freguesia
de", "associação desportiva") e, chamado por tribunal e por dia, enterrava o
log em milhares de avisos iguais.
z<SELECT id::text, legal_name, nif FROM companies WHERE activeN)r5   r   mappingsrP   r   r   )r   empresascs   &  r   carregar_distintivosr   S  s_      ooOP
 	
 hj	 
 !8!<8a$q'8!<==		
 "=s!   A,A%)A,
A'A,'A,c          
          V ^8  d   QhR\         R\        \        ,          R\        \        \        \        \        3,          3,          R,          R\        \        \
        3,          /# )r   r   rF   rU   Nr   )r   rH   r   r   r   rI   )r   s   "r   r   r   c  sS      S	 c4S>)*T1 
#s(^	r   c                P  "   V'       g   R^ R^ R^ R^ /# Vf   \        V 4      G Rj  xL
 p\        W4      G Rj  xL
 p\        W4      G Rj  xL
 p\        WV4      G Rj  xL
 p\	        WV,           4      G Rj  xL
 pRVR\        V4      R\        V4      RV/#  Lt Lc LR L@ L(5i)z7Cruza um conjunto de processos do arquivo. Idempotente.nifshits_nif	hits_nome	processosN)r   rM   rS   rk   r   len)r   rF   rU   r   por_nifpor_nomer   s   &&&    r   match_filingsr   c  s      :q+q+qII099#G88D!'66G#GEEH)'X3EFFICLS]Y	  :86EFsU   %B&BB&BB&B B&!B""B&;B$<!B&B& B&"B&$B&sincelimitc          
          V ^8  d   QhR\         R\        R,          R\        R,          R\        \        \        3,          /# )r   r   r   Nr   r   )r   r   rI   r   r   )r   s   "r   r   r   {  sB     / // $;/ :	/
 
#s(^/r   c          
       "   R^ R^ R^ R^ R^ /p\        V 4      G Rj  xL
 p^ p V P                  \        R4      RVR	\        R
V/4      G Rj  xL
 P	                  4        Uu. uF  pV^ ,          NK  	  ppV'       g   M\        WV4      G Rj  xL
 pVP                  4        F  w  rW9;;,          V
,          uu&   K  	  VR;;,          \        V4      ,          uu&   V P                  4       G Rj  xL
  V\        ,          pV\        ^
,          ,          ^ 8X  d   \        P                  RVR,          4       V'       g   EK   VR,          V8  g   EK1   \        P                  RV4       V#  ELQ EL$u upi  L L5i)uE  Reaplica o cruzamento ao arquivo inteiro, por lotes.

É a razão de existir do arquivo. Corre-se quando entra uma empresa nova — e
aí o alcance é tudo o que estiver guardado, não os 180 dias que o CITIUS
ainda serve — ou quando a regra de correspondência muda e o que falhou em
silêncio merece uma segunda leitura.
processos_vistosr   r   r   r   NaX  
                        SELECT id::text FROM court_filings
                         WHERE (CAST(:desde AS DATE) IS NULL
                                OR data_distribuicao >= CAST(:desde AS DATE))
                         ORDER BY data_distribuicao DESC NULLS LAST, id
                         LIMIT :lote OFFSET :off
                        desdeloteoffz)rematch: %d processos do arquivo revistoszrematch terminado: %s)r   r5   r   LOTErP   r   r4   r   commitloggerinfo)r   r   r   totalrU   offsetrR   rK   parcialr=   r>   s   &$$        r   rematch_allr   {  sj      FAz1k1k[\]E,W55KF
 oo eVT5&A  ce
 aDD 	 
" %gK@@MMODAHMH $ !SX-!nn$TBY1$KKCUK]E^_5U-.%7
KK'/LC 6
& A 	s_   E=E./E=E1E="E44E=E9A"E=1E;2AE=E=E=1E=4E=;E=)N)%__doc__r7   loggingdatetimer   typingr   	rapidfuzzr   
sqlalchemyr   sqlalchemy.ext.asyncior   
app.configr   app.scrapers.distribuicaor   r	   app.services.dedupr
   app.services.ingestr    app.services.registry_act_parserr   r   	getLogger__name__r   r   rC   rM   rS   rk   r   r   r   r    r   r   <module>r      s   (       /  S * . H			8	$
 F(R* Z 8Up:z> 0/ / 	/ /r   