export type Box=[number,number,number,number]; export type TextRegion={box:Box;direction:string}; /** Associate residual glyphs with an existing semantic word/paragraph, never create a second word ID. */ export function textMergeTarget(fragment:Box,regions:TextRegion[]):number{ const[x,y,w,h]=fragment;let result=-1,best=Infinity; for(const[index,r]of regions.entries()){ const[a,b,c,d]=r.box,vertical=r.direction==='vertical'; const gapX=Math.max(a-x-w,x-a-c,0),gapY=Math.max(b-y-h,y-b-d,0); const overlapX=Math.max(0,Math.min(x+w,a+c)-Math.max(x,a)),overlapY=Math.max(0,Math.min(y+h,b+d)-Math.max(y,b)); const sameBaseline=overlapY/Math.max(1,Math.min(h,d))>=.55&&gapX<=Math.max(3,Math.min(7,d*.4)); const sameColumn=overlapX/Math.max(1,Math.min(w,c))>=.55&&gapY<=Math.max(2,Math.min(4,vertical?c*.3:2)); if(!(sameBaseline||sameColumn))continue; // Nearby small footer blocks remain independent when the fragment has no baseline/column affinity. const score=gapX+gapY+(vertical?(1-overlapX/Math.max(1,Math.min(w,c))):(1-overlapY/Math.max(1,Math.min(h,d)))); if(score