= 0; $i--) {
if ((int)$paras[$i]['link_count'] <= $max_links_per_p) { $chosen = $paras[$i]; break; }
}
if ($chosen === null) $chosen = end($paras);
return (int) $chosen['end']; // ← p閉じタグの直後
}
// pが無ければ、見出し直後
return (int) $section['content_start'];
}
/** 予備:本文全体の を拾って 15/50/85% あたりの直後に差し込み位置を作る(既に確保済みは重複回避) */
private function fallback_insertion_points_by_paragraphs(string $html, int $need, array $have): array {
$out = $have;
if (!preg_match_all('/
]*>.*?<\/p>/is', $html, $pm, PREG_OFFSET_CAPTURE)) return $out;
$paras = array_map(function($m){
return ['start'=>$m[1], 'end'=>$m[1]+strlen($m[0])];
}, $pm[0]);
if (empty($paras)) return $out;
$ratios = [0.15, 0.50, 0.85];
$targets = [];
$total = count($paras);
foreach ($ratios as $r) $targets[] = $paras[(int) floor(($total-1)*$r)]['end'];
$used_offsets = array_map(fn($x)=> (int)$x['offset'], $out);
foreach ($targets as $i => $off) {
if (count($out) >= $need) break;
// 近接回避 ~80文字
$near = false;
foreach ($used_offsets as $u) { if (abs($u - $off) < 80) { $near = true; break; } }
if ($near) continue;
$out[] = ['offset'=>$off, 'zone'=>['top','mid','bot'][$i] ?? 'mid', 'ctx_heading'=>'', 'ctx_text'=>''];
$used_offsets[] = $off;
}
return array_slice($out, 0, $need);
}
/** 目標が3未満のときの尻保険:原文末尾に3つ足す(原文は維持) */
private function append_n_blocks_to_tail(string $original, WP_Post $source, array $candidate_ids, int $n): string {
$targets = [];
foreach ($candidate_ids as $cid) {
if (count($targets) >= $n) break;
$targets[] = ['post_id'=>$cid,'url'=>get_permalink($cid),'title'=>get_the_title($cid)];
}
if (empty($targets)) return $original;
$result = $original;
for ($i=0; $i<$n; $i++) {
$tg = $targets[$i % count($targets)];
$meta = $this->fetch_dest_meta($tg['url'], $tg['post_id'], $tg['title']);
$sentence = $this->fallback_sentence_with_meta('本文の補足', $meta, $tg['url']);
$result .= '
'.$this->force_link_attrs($sentence).'
';
}
return $result;
}
/**
* 各ゾーン文脈に最も近い遷移先を選ぶ(同タグ/自リンク以外/重複禁止)
* return: [ ['block_index'=>int(dummy),'post_id'=>int,'url'=>string,'title'=>string,'score'=>float], ... ]
*/
private function pick_targets_for_sections(WP_Post $source, array $sections, array $sec_indices, array $candidate_ids, int $need): array {
$cands = [];
foreach ($candidate_ids as $cid) {
$p = get_post($cid); if (!$p) continue;
$url = get_permalink($p);
$meta = $this->fetch_dest_meta($url, $cid, get_the_title($p));
$tokens = $this->tokenize_to_set($meta['title'].' '.$meta['desc'].' '.implode(' ', $meta['heads']));
$cands[$cid] = ['post_id'=>$cid, 'url'=>$url, 'title'=>get_the_title($cid), 'tokens'=>$tokens];
}
if (empty($cands)) return [];
$used = [];
$out = [];
$sim_th = (float) get_option(self::OPTION_SIM_THRESHOLD, 0.02);
foreach ($sec_indices as $sidx) {
$ctx = ($sections[$sidx]['h_text'] ?? '');
if (!empty($sections[$sidx]['paras'])) {
$ctx .= ' '.implode(' ', array_column($sections[$sidx]['paras'], 'text'));
}
$ctx_tokens = $this->tokenize_to_set($ctx);
$best_cid = null; $best_sc = -1;
foreach ($cands as $cid => $info) {
if (in_array($cid, $used, true)) continue;
$sc = $this->jaccard($ctx_tokens, $info['tokens']);
// 見出し類との近さも加点
$sc += 0.05 * $this->jaccard(
$this->tokenize_to_set($source->post_title.' '.$this->extract_headings_text($source->post_content)),
$info['tokens']
);
if ($sc > $best_sc) { $best_sc = $sc; $best_cid = $cid; }
}
if ($best_cid !== null && ($best_sc >= $sim_th || empty($out))) {
$out[] = ['block_index'=>0, 'post_id'=>$best_cid, 'url'=>$cands[$best_cid]['url'], 'title'=>$cands[$best_cid]['title'], 'score'=>$best_sc];
$used[] = $best_cid;
}
if (count($out) >= $need) break;
}
// 足りなければ全体近傍から補完
if (count($out) < $need) {
$global = $this->tokenize_to_set(
$source->post_title.' '.$this->extract_headings_text($source->post_content).' '.wp_strip_all_tags($source->post_content)
);
$rank = [];
foreach ($cands as $cid => $info) {
if (in_array($cid, $used, true)) continue;
$rank[$cid] = $this->jaccard($global, $info['tokens']);
}
arsort($rank, SORT_NUMERIC);
foreach (array_keys($rank) as $cid) {
if (count($out) >= $need) break;
$out[] = ['block_index'=>0, 'post_id'=>$cid, 'url'=>$cands[$cid]['url'], 'title'=>$cands[$cid]['title'], 'score'=>$rank[$cid]];
$used[] = $cid;
}
}
return array_slice($out, 0, $need);
}