Skip to main content

docvortex_core/
inline_pairs.rs

1//! 行内上下标保序配对;仅计算数值关系,文字和对象物化留在 Python。
2use crate::geometry::Box4;
3
4pub type Record = (Box4, f64, f64, usize, bool, usize, usize);
5type Choice = (f64, usize, bool, bool);
6
7/// 复现主体与小字号框的低重叠外置判断。
8fn detached(a: Box4, b: Box4, h: f64) -> bool {
9    let width = 0.0_f64.max(a[2] - a[0]);
10    let edge = (b[0] - a[2]).abs().min((a[0] - b[2]).abs());
11    let gap = 0.0_f64.max(b[1] - a[3]).max(a[1] - b[3]);
12    let center = ((a[1] + a[3]) / 2.0 - (b[1] + b[3]) / 2.0).abs();
13    let outside = (b[1] - a[1]).max(a[3] - b[3]);
14    width <= 0.75 * h
15        && edge <= 1.0_f64.max(0.1 * h)
16        && gap <= 0.5_f64.max(0.15 * h)
17        && center >= 0.5_f64.max(0.25 * h)
18        && outside >= 0.5_f64.max(0.2 * h)
19}
20
21/// 为已验证记录计算候选度量;布尔位置 true 表示前缀。
22fn candidate(a: &Record, b: &Record) -> Option<(f64, bool, bool)> {
23    if a.5 == b.5 || a.6 != b.6 || a.1 <= 0.0 || b.1 <= 0.0 {
24        return None;
25    }
26    let legacy_a = 0.1_f64.max(a.1);
27    let legacy_b = 0.1_f64.max(b.1);
28    let ratio = legacy_a / legacy_b;
29    let canonical = a.4 && !(0.35..=0.8).contains(&ratio) && (0.35..=0.8).contains(&(a.2 / b.2));
30    let (small, base) = if canonical {
31        (a.2, b.2)
32    } else {
33        (legacy_a, legacy_b)
34    };
35    if !(0.35..=0.8).contains(&(small / base)) || (a.3 > 8 && a.0[2] - a.0[0] > 3.0 * base) {
36        return None;
37    }
38    let overlap = 0.0_f64.max(a.0[3].min(b.0[3]) - a.0[1].max(b.0[1]));
39    let overlap_ratio = overlap / 0.1_f64.max(a.0[3] - a.0[1]);
40    let outside_pair = overlap_ratio < 0.5 && detached(a.0, b.0, base);
41    if overlap_ratio < 0.5 && !outside_pair {
42        return None;
43    }
44    let center = ((a.0[1] + a.0[3]) / 2.0 - (b.0[1] + b.0[3]) / 2.0).abs();
45    if center < 0.5_f64.max(0.12 * base) {
46        return None;
47    }
48    let mut best: Option<(f64, bool, f64)> = None;
49    for (prefix, gap) in [(true, b.0[0] - a.0[2]), (false, a.0[0] - b.0[2])] {
50        if gap >= -0.35 * base
51            && gap <= 1.5_f64.max(0.35 * base)
52            && best.is_none_or(|old| gap.abs() < old.0)
53        {
54            best = Some((gap.abs(), prefix, gap));
55        }
56    }
57    let (_, prefix, gap) = best?;
58    let outside = (b.0[1] - a.0[1]).max(a.0[3] - b.0[3]);
59    if outside < 0.5_f64.max(0.08 * base) && gap.abs() > 1.0_f64.max(0.1 * base) {
60        return None;
61    }
62    Some((
63        gap.abs() + (1.0 - overlap_ratio) * base,
64        prefix,
65        outside_pair,
66    ))
67}
68
69/// 每页建立边缘索引,严格按小行和主体原索引裁决相同度量,存储规模为线性。
70pub fn matches(records: Vec<Record>) -> Option<Vec<(usize, usize, bool, bool)>> {
71    let n = records.len();
72    if records.iter().any(|r| {
73        r.6 >= n
74            || r.0.iter().any(|x| !x.is_finite() || x.abs() > 1e100)
75            || r.0[2] <= r.0[0]
76            || r.0[3] <= r.0[1]
77            || !r.1.is_finite()
78            || r.1.abs() > 1e100
79            || !r.2.is_finite()
80            || r.2 <= 0.0
81            || r.2 > 1e100
82    }) {
83        return None;
84    }
85    let count = records.iter().map(|r| r.6).max().map_or(0, |x| x + 1);
86    let mut left = vec![Vec::new(); count];
87    let mut right = left.clone();
88    let mut scales = vec![0.1_f64; count];
89    for (i, r) in records.iter().enumerate() {
90        left[r.6].push((r.0[0], i));
91        right[r.6].push((r.0[2], i));
92        scales[r.6] = scales[r.6].max(r.2).max(0.1_f64.max(r.1));
93    }
94    for values in left.iter_mut().chain(right.iter_mut()) {
95        values.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap().then(a.1.cmp(&b.1)));
96    }
97    let mut best_base: Vec<Option<Choice>> = vec![None; n];
98    let mut best_small: Vec<[Option<(f64, usize)>; 2]> = vec![[None, None]; n];
99    for (i, a) in records.iter().enumerate() {
100        if a.3 == 0 {
101            continue;
102        }
103        let padding = 0.35 * scales[a.6];
104        let gap = 1.5_f64.max(padding);
105        let mut bases = Vec::new();
106        for (values, low, high) in [
107            (&left[a.6], a.0[2] - padding, a.0[2] + gap),
108            (&right[a.6], a.0[0] - gap, a.0[0] + padding),
109        ] {
110            let start = values.partition_point(|v| v.0 < low);
111            let end = values.partition_point(|v| v.0 <= high);
112            bases.extend(values[start..end].iter().map(|v| v.1));
113        }
114        bases.sort_unstable();
115        bases.dedup();
116        for j in bases {
117            if i == j {
118                continue;
119            }
120            if let Some((metric, prefix, outside)) = candidate(a, &records[j]) {
121                if best_base[i].is_none_or(|old| metric < old.0) {
122                    best_base[i] = Some((metric, j, prefix, outside));
123                }
124                let slot = usize::from(!prefix);
125                if best_small[j][slot].is_none_or(|old| metric < old.0) {
126                    best_small[j][slot] = Some((metric, i));
127                }
128            }
129        }
130    }
131    Some(
132        best_base
133            .iter()
134            .enumerate()
135            .filter_map(|(i, best)| {
136                let (_, base, prefix, outside) = (*best)?;
137                (best_small[base][usize::from(!prefix)].map(|x| x.1) == Some(i))
138                    .then_some((i, base, prefix, outside))
139            })
140            .collect(),
141    )
142}