/* Portable PDF text search that also reports approximate displayed bounding
 * boxes. Coordinates use the rotated CropBox, a top-left origin, and include
 * UserUnit. No OS PDF framework, external command, or OCR engine is used. */
typedef struct {
    unsigned int cp;
    double left, top, right, bottom;
    int has_box;
} B2026PdfCoordGlyph;
typedef struct {
    B2026PdfCoordGlyph *items;
    int count, capacity;
} B2026PdfCoordGlyphs;
typedef struct {
    char *text;
    B2026PdfCoordGlyph *box_by_byte;
    size_t length;
} B2026PdfCoordNormalized;
typedef struct {
    int page;
    double left, top, right, bottom, page_width, page_height;
} B2026PdfCoordMatch;
typedef struct {
    B2026PdfCoordMatch *items;
    int count, capacity;
} B2026PdfCoordMatches;

static int b2026_pdf_coord_glyph_add(B2026PdfCoordGlyphs *g,unsigned int cp,double l,double t,double r,double b,int has_box){
    B2026PdfCoordGlyph *p;
    if(g->count==g->capacity){int cap=g->capacity==0?64:g->capacity*2;p=(B2026PdfCoordGlyph*)realloc(g->items,(size_t)cap*sizeof(*p));if(p==NULL)return 0;g->items=p;g->capacity=cap;}
    g->items[g->count].cp=cp;g->items[g->count].left=l;g->items[g->count].top=t;g->items[g->count].right=r;g->items[g->count].bottom=b;g->items[g->count].has_box=has_box;++g->count;return 1;
}
static int b2026_pdf_coord_utf8(unsigned int cp,char out[5]){
    if(cp<=0x7FU){out[0]=(char)cp;out[1]='\0';return 1;}
    if(cp<=0x7FFU){out[0]=(char)(0xC0U|(cp>>6));out[1]=(char)(0x80U|(cp&0x3FU));out[2]='\0';return 2;}
    if(cp<=0xFFFFU){out[0]=(char)(0xE0U|(cp>>12));out[1]=(char)(0x80U|((cp>>6)&0x3FU));out[2]=(char)(0x80U|(cp&0x3FU));out[3]='\0';return 3;}
    if(cp>0x10FFFFU)cp=0xFFFDU;out[0]=(char)(0xF0U|(cp>>18));out[1]=(char)(0x80U|((cp>>12)&0x3FU));out[2]=(char)(0x80U|((cp>>6)&0x3FU));out[3]=(char)(0x80U|(cp&0x3FU));out[4]='\0';return 4;
}
static void b2026_pdf_coord_box_for_char(const B2026PdfTextRun *r,int rotation,double offset,double advance,double *l,double *t,double *rr,double *b){
    double size=fabs(r->size);if(size<0.1)size=0.1;rotation=((rotation%360)+360)%360;
    if(rotation==90){*l=r->x-size*0.20;*rr=r->x+size*0.80;*t=r->y+offset;*b=*t+advance;}
    else if(rotation==180){*rr=r->x-offset;*l=*rr-advance;*t=r->y-size*0.20;*b=r->y+size*0.80;}
    else if(rotation==270){*l=r->x-size*0.80;*rr=r->x+size*0.20;*b=r->y-offset;*t=*b-advance;}
    else{*l=r->x+offset;*rr=*l+advance;*t=r->y-size*0.80;*b=r->y+size*0.20;}
    if(*l>*rr){double q=*l;*l=*rr;*rr=q;}if(*t>*b){double q=*t;*t=*b;*b=q;}
}
static int b2026_pdf_coord_page_glyphs(B2026PdfDocument *doc,int page,const char *path,int *partial,B2026PdfCoordGlyphs *glyphs,double *page_width,double *page_height){
    int oi=doc->page_objects[page],rotation,replacement=0,i;const unsigned char *cv;size_t cn;B2026PdfTextBuffer content;B2026PdfExtractFont *fonts=NULL;int font_count=0;B2026PdfTextRuns runs;double box[4],tol,previous_y=0.0,previous_end=0.0,user_unit=1.0;
    memset(&content,0,sizeof(content));memset(&runs,0,sizeof(runs));memset(glyphs,0,sizeof(*glyphs));
    if(!b2026_pdf_text_fonts(doc,oi,&fonts,&font_count))goto partial_page;
    if(!b2026_pdf_tf_inherited_value(doc,oi,"/Contents",&cv,&cn))goto empty_page;
    if(!b2026_pdf_tf_collect_content(doc,cv,cn,&content,0))goto empty_page;
    if(!b2026_pdf_text_content(&runs,fonts,font_count,(const unsigned char*)content.data,content.used,&replacement))goto partial_page;
    if(!b2026_pdf_page_box(doc,oi,"/CropBox",box)&&!b2026_pdf_page_box(doc,oi,"/MediaBox",box)){box[0]=box[1]=0.0;box[2]=612.0;box[3]=792.0;}
    if(b2026_pdf_page_direct_number(doc,oi,"/UserUnit",&user_unit)<=0||!isfinite(user_unit)||user_unit<=0.0)user_unit=1.0;
    rotation=b2026_pdf_page_inherited_integer(doc,oi,"/Rotate",0);rotation=((rotation%360)+360)%360;
    *page_width=((rotation==90||rotation==270)?box[3]-box[1]:box[2]-box[0])*user_unit;
    *page_height=((rotation==90||rotation==270)?box[2]-box[0]:box[3]-box[1])*user_unit;
    for(i=0;i<runs.count;++i)b2026_pdf_text_display(&runs.items[i],box,rotation);
    tol=(runs.count>0?runs.items[0].size:12.0)*0.35;if(tol<0.5)tol=0.5;b2026_pdf_text_sort(&runs,tol);
    for(i=0;i<runs.count;++i){
        B2026PdfTextRun *r=&runs.items[i];const unsigned char *p=(const unsigned char*)r->text;double offset=0.0;
        if(i>0){int new_line=fabs(r->y-previous_y)>tol;if(new_line){if(!b2026_pdf_coord_glyph_add(glyphs,'\n',0,0,0,0,0))goto memory_fail;}else if(r->x-previous_end>(r->size*0.20)){if(!b2026_pdf_coord_glyph_add(glyphs,' ',0,0,0,0,0))goto memory_fail;}}
        while(*p){int span=utf8_char_span(p);unsigned int cp;char one[5];double advance,l,t,rr,b;if(span<1)span=1;cp=utf8_decode_codepoint(p,span);memcpy(one,p,(size_t)span);one[span]='\0';advance=b2026_pdf_text_em_width(one)*fabs(r->size);if(advance<0.01)advance=fabs(r->size)*0.01;b2026_pdf_coord_box_for_char(r,rotation,offset,advance,&l,&t,&rr,&b);if(!b2026_pdf_coord_glyph_add(glyphs,cp,l*user_unit,t*user_unit,rr*user_unit,b*user_unit,1))goto memory_fail;offset+=advance;p+=(size_t)span;}
        previous_y=r->y;previous_end=r->end_x;
    }
    if(replacement){if((*partial&1)==0)b2026_pdf_text_extract_warning(path,"one or more character codes could not be mapped and were replaced with U+FFFD / 一部の文字を復号できないためU+FFFDへ置換しました");*partial|=1;}
    free(content.data);b2026_pdf_text_fonts_free(fonts,font_count);b2026_pdf_text_runs_free(&runs);return 1;
partial_page:
    if((*partial&2)==0)b2026_pdf_text_extract_warning(path,"page content or font mapping is unsupported; partial coordinate search continues / 未対応のページ内容またはフォント対応表があるため座標検索を部分的に継続します");*partial|=2;
empty_page:
    if(!b2026_pdf_page_box(doc,oi,"/CropBox",box)&&!b2026_pdf_page_box(doc,oi,"/MediaBox",box)){box[0]=box[1]=0.0;box[2]=612.0;box[3]=792.0;}
    if(b2026_pdf_page_direct_number(doc,oi,"/UserUnit",&user_unit)<=0||!isfinite(user_unit)||user_unit<=0.0)user_unit=1.0;rotation=b2026_pdf_page_inherited_integer(doc,oi,"/Rotate",0);rotation=((rotation%360)+360)%360;*page_width=((rotation==90||rotation==270)?box[3]-box[1]:box[2]-box[0])*user_unit;*page_height=((rotation==90||rotation==270)?box[2]-box[0]:box[3]-box[1])*user_unit;
    free(content.data);b2026_pdf_text_fonts_free(fonts,font_count);b2026_pdf_text_runs_free(&runs);return 1;
memory_fail:
    free(glyphs->items);memset(glyphs,0,sizeof(*glyphs));free(content.data);b2026_pdf_text_fonts_free(fonts,font_count);b2026_pdf_text_runs_free(&runs);return 0;
}
static void b2026_pdf_coord_union(B2026PdfCoordGlyph *a,const B2026PdfCoordGlyph *b){if(!b->has_box)return;if(!a->has_box){*a=*b;return;}if(b->left<a->left)a->left=b->left;if(b->top<a->top)a->top=b->top;if(b->right>a->right)a->right=b->right;if(b->bottom>a->bottom)a->bottom=b->bottom;}
static int b2026_pdf_coord_normalize(const B2026PdfCoordGlyphs *source,const B2026PdfSearchSettings *s,B2026PdfCoordNormalized *out){
    B2026PdfTextBuffer text;B2026PdfCoordGlyph *map=NULL;size_t map_count=0U;int i;memset(&text,0,sizeof(text));memset(out,0,sizeof(*out));
    for(i=0;i<source->count;++i){B2026PdfCoordGlyph box=source->items[i];unsigned int cp=box.cp,combined=0U;char bytes[5];int n,j;if(s->ignore_width){cp=b2026_pdf_search_width_fold(cp);if(i+1<source->count){unsigned int next=b2026_pdf_search_width_fold(source->items[i+1].cp);combined=b2026_pdf_search_kana_compose(cp,next);if(combined!=0U){cp=combined;b2026_pdf_coord_union(&box,&source->items[i+1]);++i;}}}if(!s->case_sensitive)cp=b2026_pdf_search_case_fold(cp);if(s->ignore_whitespace&&b2026_pdf_search_space(cp))continue;n=b2026_pdf_coord_utf8(cp,bytes);{B2026PdfCoordGlyph *q=(B2026PdfCoordGlyph*)realloc(map,(map_count+(size_t)n)*sizeof(*map));if(q==NULL)goto fail;map=q;}if(!b2026_pdf_text_append_bytes(&text,bytes,(size_t)n))goto fail;for(j=0;j<n;++j)map[map_count++]=box;}
    out->text=b2026_pdf_text_take(&text);out->box_by_byte=map;out->length=map_count;return 1;
fail:free(text.data);free(map);return 0;
}
static int b2026_pdf_coord_match_add(B2026PdfCoordMatches *m,int page,double l,double t,double r,double b,double pw,double ph){B2026PdfCoordMatch *p;if(m->count>=100000)return -1;if(m->count==m->capacity){int cap=m->capacity==0?32:m->capacity*2;p=(B2026PdfCoordMatch*)realloc(m->items,(size_t)cap*sizeof(*p));if(p==NULL)return 0;m->items=p;m->capacity=cap;}m->items[m->count].page=page;m->items[m->count].left=l;m->items[m->count].top=t;m->items[m->count].right=r;m->items[m->count].bottom=b;m->items[m->count].page_width=pw;m->items[m->count].page_height=ph;++m->count;return 1;}
static int b2026_pdf_coord_find_page(const B2026PdfCoordNormalized *n,const char *needle,int page,double pw,double ph,B2026PdfCoordMatches *matches){
    const char *cursor=n->text!=NULL?n->text:"";size_t needle_len=strlen(needle);while(*cursor){const char *hit=strstr(cursor,needle);size_t start,end,k;B2026PdfCoordGlyph box;int added;if(hit==NULL)break;start=(size_t)(hit-(n->text!=NULL?n->text:""));end=start+needle_len;memset(&box,0,sizeof(box));for(k=start;k<end&&k<n->length;++k)b2026_pdf_coord_union(&box,&n->box_by_byte[k]);if(box.has_box){added=b2026_pdf_coord_match_add(matches,page,box.left,box.top,box.right,box.bottom,pw,ph);if(added<=0)return added;}cursor=hit+needle_len;}return 1;
}
static int b2026_pdf_coord_unit(const B2026PdfArg *arg,double *factor,const char **name){HashVariable *h=NULL;HashEntry *e;const char *u;*factor=1.0;*name="POINT";if(arg==NULL)return 1;if(!b2026_pdf_tf_hash(arg,&h))return 0;e=b2026_pdf_tf_entry(h,"単位","unit");if(e==NULL)return 1;if(e->value.type==TYPE_NUMBER)return 0;u=hash_value_as_text(&e->value);if(b2026_strcasecmp(u,"POINT")==0||b2026_strcasecmp(u,"PT")==0||strcmp(u,"ポイント")==0)return 1;if(b2026_strcasecmp(u,"MM")==0||strcmp(u,"ミリ")==0){*factor=25.4/72.0;*name="MM";return 1;}if(b2026_strcasecmp(u,"INCH")==0||b2026_strcasecmp(u,"IN")==0||strcmp(u,"インチ")==0){*factor=1.0/72.0;*name="INCH";return 1;}return 0;}
static void b2026_pdf_coord_pair_number(const char *target,int index,const char *jp,const char *en,double value){char key[96];if(target==NULL)return;snprintf(key,sizeof(key),"%d,%s",index,jp);hash_set_number(target,key,value);snprintf(key,sizeof(key),"%d,%s",index,en);hash_set_number(target,key,value);}
static void b2026_pdf_coord_result_details(const char *target,const B2026PdfCoordMatches *matches,double factor,const char *unit){int i;if(target==NULL)return;b2026_pdf_search_pair_number(target,"該当数","matchCount",(double)matches->count);b2026_pdf_search_pair_number(target,"件数","count",(double)matches->count);b2026_pdf_search_pair_text(target,"単位","unit",unit);for(i=0;i<matches->count;++i){const B2026PdfCoordMatch *m=&matches->items[i];int n=i+1;b2026_pdf_coord_pair_number(target,n,"ページ","page",(double)m->page);b2026_pdf_coord_pair_number(target,n,"X","x",m->left*factor);b2026_pdf_coord_pair_number(target,n,"Y","y",m->top*factor);b2026_pdf_coord_pair_number(target,n,"左","left",m->left*factor);b2026_pdf_coord_pair_number(target,n,"上","top",m->top*factor);b2026_pdf_coord_pair_number(target,n,"右","right",m->right*factor);b2026_pdf_coord_pair_number(target,n,"下","bottom",m->bottom*factor);b2026_pdf_coord_pair_number(target,n,"横幅","width",(m->right-m->left)*factor);b2026_pdf_coord_pair_number(target,n,"縦幅","height",(m->bottom-m->top)*factor);b2026_pdf_coord_pair_number(target,n,"ページ横幅","pageWidth",m->page_width*factor);b2026_pdf_coord_pair_number(target,n,"ページ縦幅","pageHeight",m->page_height*factor);}}
static double b2026_pdf_coord_fail(const char *target,int code,const char *path,const char *detail,const char *status,const char *unit){char message[2048];B2026PdfCoordMatches empty;memset(&empty,0,sizeof(empty));b2026_pdf_search_result(target,0,0,0,status!=NULL?status:"FAILED",0,0,0,0,NULL,"");b2026_pdf_coord_result_details(target,&empty,1.0,unit!=NULL?unit:"POINT");snprintf(message,sizeof(message),"PDF.SEARCHTEXTCOORDS failed: path=%s: %s / 【PDF座標付き文字検索エラー】%s",path!=NULL?path:"",detail!=NULL?detail:"unknown error",detail!=NULL?detail:"処理できません");basic_set_error_logged(code,message,"PDF.SEARCHTEXTCOORDS",current_source_path,current_line,"PDF");b2026_try_append_log_line("__PDF_ERROR.log",message);return -1.0;}
static double b2026_pdf_search_text_coords(const char *target,int argc,B2026PdfArg *args){
    B2026PdfDocument doc;B2026PdfSearchSettings settings;B2026PdfArg page_arg;const char *path="",*needle="",*unit="POINT";char *normalized_needle=NULL,*page_spec=NULL;unsigned char *selected=NULL,*matched_pages=NULL;B2026PdfCoordMatches matches;double factor=1.0,result=-1.0;int rc,i,partial=0,searched=0,text_pages=0,matched_page_count=0;
    memset(&doc,0,sizeof(doc));memset(&matches,0,sizeof(matches));b2026_pdf_search_clear(target);basic_clear_error();
    if((argc!=2&&argc!=3)||args[0].kind!=1||args[1].kind!=1||args[0].text==NULL||args[1].text==NULL||(argc==3&&args[2].kind!=3)||!b2026_pdf_search_settings(argc==3?&args[2]:NULL,&settings)||!b2026_pdf_coord_unit(argc==3?&args[2]:NULL,&factor,&unit))return b2026_pdf_coord_fail(target,4370,"","path$, search-text$ [, settings{}] is required / PDFパス、検索文字列、任意の設定が必要です","FAILED",unit);
    path=args[0].text;needle=args[1].text;if(*needle=='\0')return b2026_pdf_coord_fail(target,4371,path,"search text must not be empty / 検索文字列は空にできません","FAILED",unit);normalized_needle=b2026_pdf_search_normalize(needle,&settings);if(normalized_needle==NULL||*normalized_needle=='\0'){free(normalized_needle);return b2026_pdf_coord_fail(target,4371,path,"normalized search text is empty / 正規化後の検索文字列が空です","FAILED",unit);}
    rc=b2026_pdf_load(&doc,path,0);if(rc<=0){const char *detail=b2026_pdf_load_error(rc);int timed_out=g_last_error.timeout!=0;free(normalized_needle);b2026_pdf_document_free(&doc);return b2026_pdf_coord_fail(target,4372,path,detail,timed_out?"TIMEOUT":"FAILED",unit);}
    memset(&page_arg,0,sizeof(page_arg));page_arg.kind=1;page_arg.text=settings.page_spec;if(!b2026_pdf_parse_page_spec(&page_arg,doc.page_count,&selected)){b2026_pdf_document_free(&doc);free(normalized_needle);return b2026_pdf_coord_fail(target,4373,path,"invalid target pages / 対象ページの指定が正しくありません","FAILED",unit);}matched_pages=(unsigned char*)calloc((size_t)doc.page_count,1U);if(matched_pages==NULL)goto memory_fail;
    for(i=0;i<doc.page_count;++i)if(selected[i]){B2026PdfCoordGlyphs glyphs;B2026PdfCoordNormalized normalized;double pw=0.0,ph=0.0;int before=matches.count,found;++searched;memset(&glyphs,0,sizeof(glyphs));memset(&normalized,0,sizeof(normalized));if(!b2026_pdf_coord_page_glyphs(&doc,i,path,&partial,&glyphs,&pw,&ph))goto memory_fail;if(glyphs.count>0)++text_pages;if(!b2026_pdf_coord_normalize(&glyphs,&settings,&normalized)){free(glyphs.items);goto memory_fail;}found=b2026_pdf_coord_find_page(&normalized,normalized_needle,i+1,pw,ph,&matches);free(normalized.text);free(normalized.box_by_byte);free(glyphs.items);if(found<0){b2026_pdf_coord_fail(target,4375,path,"match count exceeds 100000 / 一致件数が100000件を超えました","LIMIT_EXCEEDED",unit);goto done;}if(found==0)goto memory_fail;if(matches.count>before){matched_pages[i]=1;++matched_page_count;}}
    page_spec=b2026_pdf_search_page_spec(matched_pages,doc.page_count);if(page_spec==NULL)goto memory_fail;b2026_pdf_search_result(target,1,partial?0:1,matches.count>0,partial?"PARTIAL":(matches.count>0?"FOUND":(text_pages>0?"NOT_FOUND":"NO_TEXT")),doc.page_count,searched,text_pages,matched_page_count,matched_pages,page_spec);b2026_pdf_coord_result_details(target,&matches,factor,unit);result=(double)matches.count;goto done;
memory_fail:b2026_pdf_coord_fail(target,4374,path,"memory or search limit exceeded / メモリまたは検索上限を超えました","FAILED",unit);result=-1.0;
done:free(matches.items);free(page_spec);free(matched_pages);free(selected);free(normalized_needle);b2026_pdf_document_free(&doc);return result;
}
