Skip to main content

docvortex_core/
rule_text.rs

1//! 新增文字规则的批量几何;文字、字体等价关系与正则由 Python 预先计算。
2
3/// 框、字号、水平、非空字体、字体组、短数字、混合正文、长正文、目录条目、候选资格。
4pub type NoiseLine = (
5    [f64; 4],
6    f64,
7    bool,
8    bool,
9    usize,
10    bool,
11    bool,
12    bool,
13    bool,
14    bool,
15);
16
17/// 依照原始行序逐个判断数字,返回原有顺序的参考行;目录参考行保留稳定纵向排序。
18pub fn numeric_noise_candidates(
19    lines: &[NoiseLine],
20    visual: &[[f64; 4]],
21) -> Vec<(usize, Vec<usize>)> {
22    let mut groups = std::collections::HashMap::<usize, Vec<usize>>::new();
23    for (i, line) in lines.iter().enumerate() {
24        groups.entry(line.4).or_default().push(i);
25    }
26    let mut result = Vec::new();
27    for (i, &(bbox, height, _, _, font, _, _, _, _, candidate)) in lines.iter().enumerate() {
28        if !candidate {
29            continue;
30        }
31        let gutter = visual.iter().any(|a| {
32            visual.iter().any(|b| {
33                a[2] + 0.2 * height <= bbox[0]
34                    && bbox[2] <= b[0] - 0.2 * height
35                    && a[1].max(b[1]) <= bbox[1]
36                    && bbox[3] <= a[3].min(b[3])
37                    && a[3] - a[1] >= 8.0 * height
38                    && b[3] - b[1] >= 8.0 * height
39            })
40        });
41        if gutter {
42            let refs: Vec<_> = lines
43                .iter()
44                .enumerate()
45                .filter(|(_, p)| p.4 != font && p.6)
46                .map(|(j, _)| j)
47                .collect();
48            if refs.len() >= 3 {
49                result.push((i, refs));
50                continue;
51            }
52        }
53        let same = &groups[&font];
54        // 数字特征独立于候选资格:同字体旋转数字仍属于原有数量限制。
55        if same.len() > 4 || same.iter().any(|&j| !lines[j].5) {
56            continue;
57        }
58        let prose: Vec<_> = lines
59            .iter()
60            .enumerate()
61            .filter(|(_, p)| {
62                p.2 && p.3
63                    && p.4 != font
64                    && 0.7 * p.1 <= height
65                    && height <= 1.05 * p.1
66                    && p.7
67                    && p.0[0] <= bbox[0]
68                    && bbox[2] <= p.0[2]
69                    && ((p.0[1] + p.0[3] - bbox[1] - bbox[3]) / 2.0).abs() <= 3.0 * p.1
70            })
71            .map(|(j, _)| j)
72            .collect();
73        if prose.len() >= 2 {
74            let mut union = lines[prose[0]].0;
75            for &j in &prose[1..] {
76                let b = lines[j].0;
77                union = [
78                    union[0].min(b[0]),
79                    union[1].min(b[1]),
80                    union[2].max(b[2]),
81                    union[3].max(b[3]),
82                ];
83            }
84            let area = (bbox[2] - bbox[0]).max(0.0) * (bbox[3] - bbox[1]).max(0.0);
85            let overlap = (bbox[2].min(union[2]) - bbox[0].max(union[0])).max(0.0)
86                * (bbox[3].min(union[3]) - bbox[1].max(union[1])).max(0.0);
87            if area > 0.0 && overlap / area >= 0.95 {
88                result.push((i, prose));
89                continue;
90            }
91        }
92        let mut entries: Vec<_> = lines
93            .iter()
94            .enumerate()
95            .filter(|(_, p)| {
96                p.2 && p.3
97                    && p.4 != font
98                    && height <= 0.5 * p.1
99                    && p.8
100                    && p.0[0] <= bbox[0]
101                    && bbox[2] <= p.0[2]
102            })
103            .map(|(j, _)| j)
104            .collect();
105        if entries.len() >= 3 {
106            let em = entries
107                .iter()
108                .map(|&j| lines[j].1)
109                .reduce(|a, b| if b < a { b } else { a })
110                .unwrap();
111            entries.sort_by(|&a, &b| lines[a].0[1].partial_cmp(&lines[b].0[1]).unwrap());
112            let left_max = entries
113                .iter()
114                .map(|&j| lines[j].0[0])
115                .reduce(|a, b| if b > a { b } else { a })
116                .unwrap();
117            let left_min = entries
118                .iter()
119                .map(|&j| lines[j].0[0])
120                .reduce(|a, b| if b < a { b } else { a })
121                .unwrap();
122            if left_max - left_min <= 0.3 * em
123                && entries.windows(2).any(|p| {
124                    lines[p[0]].0[3] + 0.1 * em <= bbox[1] && bbox[3] <= lines[p[1]].0[1] - 0.1 * em
125                })
126            {
127                result.push((i, entries));
128            }
129        }
130    }
131    result
132}
133
134/// 旋转后框、原始框、canonical 字号、角度和源索引,用于公式连通判断。
135pub type FormulaLine = ([f64; 4], [f64; 4], f64, i64, i64);
136
137/// 独立公式带的邻接索引,保持候选升序;Python 继续决定种子、认领与栈遍历。
138pub fn detached_formula_neighbors(
139    boxes: &[[f64; 4]],
140    operators: &[bool],
141    em: f64,
142) -> Vec<Vec<usize>> {
143    let mut neighbors = vec![Vec::new(); boxes.len()];
144    for (i, a) in boxes.iter().enumerate() {
145        for (j, b) in boxes.iter().enumerate() {
146            if i == j {
147                continue;
148            }
149            if a[2] - a[0] > 3.0 * em
150                && b[2] - b[0] > 3.0 * em
151                && operators[i]
152                && operators[j]
153                && ((a[1] + a[3]) / 2.0 - (b[1] + b[3]) / 2.0).abs() > 0.75 * em
154            {
155                continue;
156            }
157            let xgap = (a[0] - b[2]).max(b[0] - a[2]).max(0.0);
158            let ygap = (a[1] - b[3]).max(b[1] - a[3]).max(0.0);
159            let shorter_x = (a[2] - a[0]).min(b[2] - b[0]);
160            let shorter_y = (a[3] - a[1]).min(b[3] - b[1]);
161            let xoverlap = if shorter_x > 0.0 {
162                (a[2].min(b[2]) - a[0].max(b[0])).max(0.0) / shorter_x
163            } else {
164                0.0
165            };
166            let yoverlap = if shorter_y > 0.0 {
167                (a[3].min(b[3]) - a[1].max(b[1])).max(0.0) / shorter_y
168            } else {
169                0.0
170            };
171            if ygap <= 0.85 * em && xoverlap > 0.1 || xgap <= 2.0 * em && yoverlap >= 0.2 {
172                neighbors[i].push(j);
173            }
174        }
175    }
176    neighbors
177}
178
179/// 两行数值连通规则,表格阻断始终使用原始页面框。
180fn formula_connected(a: &FormulaLine, b: &FormulaLine, tables: &[[f64; 4]]) -> bool {
181    if a.3 != b.3 {
182        return false;
183    }
184    let ac = ((a.1[0] + a.1[2]) / 2.0, (a.1[1] + a.1[3]) / 2.0);
185    let bc = ((b.1[0] + b.1[2]) / 2.0, (b.1[1] + b.1[3]) / 2.0);
186    let connector = [
187        ac.0.min(bc.0) - 0.1,
188        ac.1.min(bc.1) - 0.1,
189        ac.0.max(bc.0) + 0.1,
190        ac.1.max(bc.1) + 0.1,
191    ];
192    if tables.iter().any(|t| {
193        connector[0] < t[2] && t[0] < connector[2] && connector[1] < t[3] && t[1] < connector[3]
194    }) {
195        return false;
196    }
197    let height = a.2.max(b.2);
198    let vshort = (a.0[3] - a.0[1]).min(b.0[3] - b.0[1]);
199    let vo = if vshort > 0.0 {
200        (a.0[3].min(b.0[3]) - a.0[1].max(b.0[1])).max(0.0) / vshort
201    } else {
202        0.0
203    };
204    let vg = (a.0[1] - b.0[3]).max(b.0[1] - a.0[3]).max(0.0);
205    if vo < 0.2 && vg > 0.6 * height {
206        return false;
207    }
208    let hshort = (a.0[2] - a.0[0]).min(b.0[2] - b.0[0]);
209    let ho = if hshort > 0.0 {
210        (a.0[2].min(b.0[2]) - a.0[0].max(b.0[0])).max(0.0) / hshort
211    } else {
212        0.0
213    };
214    let hg = (a.0[0] - b.0[2]).max(b.0[0] - a.0[2]).max(0.0);
215    ho > 0.0 || hg <= 1.5 * height
216}
217
218/// 逐次扫描候选并立即追加成员,严格保留 Python 的种子、遍历顺序与重复源索引语义。
219pub fn grow_formula_component(
220    lines: &[FormulaLine],
221    seed_count: usize,
222    tables: &[[f64; 4]],
223) -> Vec<usize> {
224    let mut members: Vec<_> = (0..seed_count.min(lines.len())).collect();
225    let mut sources: std::collections::HashSet<_> = members.iter().map(|&i| lines[i].4).collect();
226    loop {
227        let before = members.len();
228        for i in seed_count..lines.len() {
229            if !sources.contains(&lines[i].4)
230                && members
231                    .iter()
232                    .any(|&j| formula_connected(&lines[j], &lines[i], tables))
233            {
234                members.push(i);
235                sources.insert(lines[i].4);
236            }
237        }
238        if before == members.len() {
239            break;
240        }
241    }
242    members.into_iter().skip(seed_count).collect()
243}
244
245/// 局部框、canonical 尺度、字体组、覆盖率、字重、基线、恢复簇、视觉行号、源索引和初始栏。
246pub type TailLine = (
247    [f64; 4],
248    f64,
249    Option<usize>,
250    f64,
251    Option<f64>,
252    Option<f64>,
253    bool,
254    Option<i64>,
255    i64,
256    usize,
257);
258
259/// 保留字体、字重、基线净空与视觉行差的短尾接纳条件。
260fn tail_accepts(a: &TailLine, b: &TailLine, lane: (f64, f64), median: f64) -> bool {
261    let height = a.1.max(b.1).max(median);
262    let width = (lane.1 - lane.0).max(0.1);
263    let fonts = a.2.is_some() && b.2.is_some() && a.3 >= 0.75 && b.3 >= 0.75 && a.2 != b.2;
264    let weights = match (a.4, b.4) {
265        (Some(x), Some(y)) => (x - y).abs() >= 100.0 && x.max(y) >= 1.15 * x.min(y),
266        _ => false,
267    };
268    if a.0[2] - a.0[0] < 0.65 * width
269        || b.0[2] - b.0[0] > 0.85 * width
270        || b.0[0] < lane.0 - 0.75 * height
271        || b.0[2] > lane.1 + 0.75 * height
272        || (b.0[0] - a.0[0]).abs() > 0.75 * height
273        || fonts
274        || weights
275    {
276        return false;
277    }
278    let mut gap = if a.6 {
279        (b.0[1] - a.0[3]).max(-0.25 * a.1)
280    } else {
281        b.0[1] - (a.0[1] + a.1)
282    };
283    if let (Some(x), Some(y)) = (a.5, b.5) {
284        if y > x {
285            let pitch = y - x;
286            if 0.5 * a.1.min(b.1) <= pitch && pitch <= 3.0 * a.1.max(b.1) {
287                gap = pitch - a.1;
288            }
289        }
290    }
291    if gap < -0.25 * height || gap > 0.9 * height {
292        return false;
293    }
294    if let (Some(x), Some(y)) = (a.7, b.7) {
295        let delta = i128::from(y) - i128::from(x);
296        if delta <= 0 || delta > 2 {
297            return false;
298        }
299    }
300    true
301}
302
303/// 严格较小顶边才更新前序证据;同高行批量结算后再更新,平局按源索引保持稳定。
304pub fn short_tail_destinations(
305    lines: &[TailLine],
306    lanes: &[(f64, f64)],
307    median: f64,
308) -> Vec<(usize, usize)> {
309    let mut order: Vec<_> = (0..lines.len()).collect();
310    order.sort_by(|&a, &b| {
311        lines[a].0[1]
312            .partial_cmp(&lines[b].0[1])
313            .unwrap()
314            .then_with(|| lines[a].0[0].partial_cmp(&lines[b].0[0]).unwrap())
315            .then_with(|| lines[a].8.cmp(&lines[b].8))
316    });
317    let mut previous: Vec<Option<usize>> = vec![None; lanes.len()];
318    let mut moves = Vec::new();
319    let mut cursor = 0;
320    while cursor < order.len() {
321        let mut end = cursor + 1;
322        while end < order.len() && lines[order[end]].0[1] == lines[order[cursor]].0[1] {
323            end += 1;
324        }
325        let mut settled = Vec::new();
326        for &i in &order[cursor..end] {
327            let matches: Vec<_> = previous
328                .iter()
329                .enumerate()
330                .filter(|(lane, p)| {
331                    p.is_some_and(|j| tail_accepts(&lines[j], &lines[i], lanes[*lane], median))
332                })
333                .map(|(lane, _)| lane)
334                .collect();
335            let target = if matches.len() == 1 {
336                matches[0]
337            } else {
338                lines[i].9
339            };
340            if target != lines[i].9 {
341                moves.push((i, target));
342            }
343            settled.push((i, target));
344        }
345        for (i, target) in settled {
346            let newer = previous[target].is_none_or(|j| {
347                lines[i].0[1] > lines[j].0[1]
348                    || lines[i].0[1] == lines[j].0[1]
349                        && (lines[i].0[0] > lines[j].0[0]
350                            || lines[i].0[0] == lines[j].0[0] && lines[i].8 < lines[j].8)
351            });
352            if newer {
353                previous[target] = Some(i);
354            }
355        }
356        cursor = end;
357    }
358    moves
359}
360
361/// 保留原生行组插入顺序、首个匹配组和平局顺序;均值由调用方提供原运行时的精确求和。
362pub fn fragment_row_groups<E>(
363    items: &[(f64, f64, Option<i64>)],
364    tolerance: f64,
365    mut mean: impl FnMut(&[usize]) -> Result<f64, E>,
366) -> Result<Vec<Vec<usize>>, E> {
367    let mut seeds = Vec::<Vec<usize>>::new();
368    let mut native = std::collections::HashMap::new();
369    let mut geometric = Vec::new();
370    for (i, item) in items.iter().enumerate() {
371        if let Some(id) = item.2 {
372            let next = seeds.len();
373            let group = *native.entry(id).or_insert_with(|| {
374                seeds.push(Vec::new());
375                next
376            });
377            seeds[group].push(i);
378        } else {
379            geometric.push(vec![i]);
380        }
381    }
382    seeds.extend(geometric);
383    let mut prepared = Vec::new();
384    for group in seeds {
385        let center = mean(&group)?;
386        let left = group
387            .iter()
388            .map(|&i| items[i].1)
389            .reduce(|a, b| if b < a { b } else { a })
390            .unwrap();
391        prepared.push((group, center, left));
392    }
393    prepared.sort_by(|a, b| {
394        a.1.partial_cmp(&b.1)
395            .unwrap()
396            .then_with(|| a.2.partial_cmp(&b.2).unwrap())
397    });
398    let mut groups = Vec::<Vec<usize>>::new();
399    let mut centers = Vec::<f64>::new();
400    for (seed, center, _) in prepared {
401        if let Some(target) = centers
402            .iter()
403            .position(|&other| (center - other).abs() <= tolerance)
404        {
405            groups[target].extend(seed);
406            centers[target] = mean(&groups[target])?;
407        } else {
408            groups.push(seed);
409            centers.push(center);
410        }
411    }
412    Ok(groups)
413}
414
415/// 弱表格区间的字号、上下边界、完整边框与源成员;最终认领结果不跨调用缓存。
416pub type ProseRuleDraft = (f64, f64, f64, [f64; 4], Vec<i64>);
417/// 正文的框、源编号和 Python 已计算的文字/语义准入特征。
418pub type ProseRuleLine = ([f64; 4], i64, bool);
419
420/// 相同网格和字号只收集一次结构与正文几何,原阈值与严格边界比较保持不变。
421pub fn prose_rule_conflicts(
422    drafts: &[ProseRuleDraft],
423    grids: &[[f64; 4]],
424    rules: &[([f64; 4], bool)],
425    lines: &[ProseRuleLine],
426) -> Vec<bool> {
427    let mut geometry = std::collections::HashMap::<(usize, u64), Option<Vec<(i64, f64)>>>::new();
428    drafts
429        .iter()
430        .map(|draft| {
431            let (em, first, last, boundary, members) = draft;
432            let members: std::collections::HashSet<_> = members.iter().copied().collect();
433            for (grid_index, core) in grids.iter().enumerate() {
434                let width = core[2] - core[0];
435                let height = core[3] - core[1];
436                if width < 8.0 * em
437                    || height < 4.0 * em
438                    || core[1] - first < 5.0 * em
439                    || !(core[1] - em <= *last && *last <= core[3] + em)
440                    || overlap_ratio(boundary, core, 0) < 0.9
441                {
442                    continue;
443                }
444                let evidence = geometry
445                    .entry((grid_index, em.to_bits()))
446                    .or_insert_with(|| {
447                        let mut verticals: Vec<_> = rules
448                            .iter()
449                            .filter(|(b, vertical)| {
450                                let x = (b[0] + b[2]) / 2.0;
451                                *vertical
452                                    && core[0] - 0.5 * em <= x
453                                    && x <= core[2] + 0.5 * em
454                                    && b[1] <= core[1] + 0.5 * em
455                                    && b[3] >= core[3] - 0.5 * em
456                            })
457                            .map(|(b, _)| (b[0] + b[2]) / 2.0)
458                            .collect();
459                        verticals.sort_by(|a, b| a.partial_cmp(b).unwrap());
460                        let mut tracks = Vec::new();
461                        for x in verticals {
462                            if tracks.last().is_none_or(|&last| x - last > 0.3 * em) {
463                                tracks.push(x);
464                            }
465                        }
466                        if tracks.len() < 3
467                            || ![core[1], core[3]].into_iter().all(|y| {
468                                rules.iter().any(|(b, vertical)| {
469                                    !*vertical
470                                        && overlap_ratio(b, core, 0) >= 0.9
471                                        && ((b[1] + b[3]) / 2.0 - y).abs() <= 0.5 * em
472                                })
473                            })
474                        {
475                            return None;
476                        }
477                        Some(
478                            lines
479                                .iter()
480                                .filter(|(b, _, eligible)| {
481                                    *eligible
482                                        && b[2] - b[0] >= 0.65 * width
483                                        && overlap_ratio(b, core, 0) >= 0.9
484                                })
485                                .map(|(b, source, _)| (*source, (b[1] + b[3]) / 2.0))
486                                .collect(),
487                        )
488                    });
489                let Some(prose) = evidence else {
490                    continue;
491                };
492                let mut centers: Vec<_> = prose
493                    .iter()
494                    .filter(|(source, y)| {
495                        members.contains(source) && first + 0.5 * em < *y && *y < core[1] - em
496                    })
497                    .map(|(_, y)| *y)
498                    .collect();
499                centers.sort_by(|a, b| a.partial_cmp(b).unwrap());
500                let mut rows = Vec::new();
501                for y in centers {
502                    if rows.last().is_none_or(|&last| y - last > 0.6 * em) {
503                        rows.push(y);
504                    }
505                }
506                if rows.len() >= 3 {
507                    return true;
508                }
509            }
510            false
511        })
512        .collect()
513}
514
515/// 与 Python 一致,以较短轴长作分母;退化或倒置轴的覆盖率为零。
516fn overlap_ratio(a: &[f64; 4], b: &[f64; 4], axis: usize) -> f64 {
517    let overlap = (a[axis + 2].min(b[axis + 2]) - a[axis].max(b[axis])).max(0.0);
518    let shorter = (a[axis + 2] - a[axis]).min(b[axis + 2] - b[axis]);
519    if shorter > 0.0 {
520        overlap / shorter
521    } else {
522        0.0
523    }
524}
525
526/// 先检查完整网格集合的必要几何条件,尚不读取正文成员;没有匹配网格的候选不可能冲突。
527pub fn prose_rule_eligible(drafts: &[ProseRuleDraft], grids: &[[f64; 4]]) -> Vec<bool> {
528    drafts
529        .iter()
530        .map(|(em, first, last, boundary, _)| {
531            grids.iter().any(|core| {
532                core[2] - core[0] >= 8.0 * em
533                    && core[3] - core[1] >= 4.0 * em
534                    && core[1] - first >= 5.0 * em
535                    && core[1] - em <= *last
536                    && *last <= core[3] + em
537                    && overlap_ratio(boundary, core, 0) >= 0.9
538            })
539        })
540        .collect()
541}
542
543/// 栏推断的完整框、当前字体尺度、锚点资格及嵌套栏回退资格。
544pub type LaneGeometry = ([f64; 4], f64, bool, bool);
545
546/// 左边缘已排序,逐步中位数只需读取完整当前组的中间元素,保留原贪心聚类顺序。
547pub fn supported_lane_intervals(
548    lines: &[LaneGeometry],
549    width: f64,
550    median: f64,
551) -> Vec<(f64, f64, usize)> {
552    let tolerance = 3.0f64.max(0.75 * median);
553    let mut regular: Vec<_> = (0..lines.len())
554        .filter(|&i| {
555            lines[i].2 && lines[i].0[2] - lines[i].0[0] >= (4.0 * lines[i].1).max(0.15 * width)
556        })
557        .collect();
558    regular.sort_by(|&a, &b| lines[a].0[0].partial_cmp(&lines[b].0[0]).unwrap());
559    let mut clusters = Vec::<Vec<usize>>::new();
560    for &index in &regular {
561        if let Some(cluster) = clusters.last_mut() {
562            let n = cluster.len();
563            let center = if n % 2 == 1 {
564                lines[cluster[n / 2]].0[0]
565            } else {
566                (lines[cluster[n / 2 - 1]].0[0] + lines[cluster[n / 2]].0[0]) / 2.0
567            };
568            if (lines[index].0[0] - center).abs() <= tolerance {
569                cluster.push(index);
570                continue;
571            }
572        }
573        clusters.push(vec![index]);
574    }
575    let mut supported = Vec::new();
576    for cluster in clusters.iter().filter(|c| c.len() >= 3) {
577        let left = crate::median(cluster.iter().map(|&i| lines[i].0[0]).collect());
578        let right = crate::median(cluster.iter().map(|&i| lines[i].0[2]).collect());
579        supported.push((left, right, cluster.len()));
580    }
581    supported.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
582    let mut filtered: Vec<(f64, f64, usize)> = Vec::new();
583    for interval in supported {
584        if let Some(previous) = filtered.last_mut() {
585            if interval.0 - previous.1 >= 6.0f64.max(0.75 * median) {
586                filtered.push(interval);
587            } else if interval.2 > previous.2 {
588                *previous = interval;
589            }
590        } else {
591            filtered.push(interval);
592        }
593    }
594    if filtered.is_empty() && !lines.is_empty() {
595        let source: Vec<_> = if regular.is_empty() {
596            (0..lines.len()).collect()
597        } else {
598            regular
599        };
600        let left = source
601            .iter()
602            .map(|&i| lines[i].0[0])
603            .reduce(|a, b| if b < a { b } else { a })
604            .unwrap();
605        let right = source
606            .iter()
607            .map(|&i| lines[i].0[2])
608            .reduce(|a, b| if b > a { b } else { a })
609            .unwrap();
610        filtered.push((left, right, source.len()));
611    }
612    filtered
613}
614
615/// 按原栏顺序保留首个最佳覆盖;邻栏、同高和跨栏边界沿用原严格比较。
616pub fn lane_assignments(
617    lines: &[LaneGeometry],
618    lanes: &[(f64, f64)],
619    tolerance: f64,
620    nested: Option<(f64, f64)>,
621) -> Vec<Option<usize>> {
622    let mut ordered: Vec<_> = (0..lanes.len()).collect();
623    ordered.sort_by(|&a, &b| lanes[a].0.partial_cmp(&lanes[b].0).unwrap());
624    lines
625        .iter()
626        .map(|line| {
627            let bbox = line.0;
628            if nested.is_some_and(|(top, bottom)| {
629                line.3
630                    || !((top <= (bbox[1] + bbox[3]) / 2.0)
631                        && ((bbox[1] + bbox[3]) / 2.0 <= bottom))
632            }) {
633                return Some(lanes.len());
634            }
635            let width = 0.1f64.max(bbox[2] - bbox[0]);
636            let mut best = 0;
637            let mut first = -1.0;
638            let mut second = -1.0;
639            for (i, &(left, right)) in lanes.iter().enumerate() {
640                let coverage = (bbox[2].min(right) - bbox[0].max(left)).max(0.0) / width;
641                if coverage > first {
642                    second = first;
643                    first = coverage;
644                    best = i;
645                } else if coverage > second {
646                    second = coverage;
647                }
648            }
649            let position = ordered.iter().position(|&i| i == best).unwrap();
650            let fit = !(position > 0 && bbox[0] < lanes[ordered[position - 1]].1 - tolerance
651                || position + 1 < ordered.len()
652                    && bbox[2] > lanes[ordered[position + 1]].0 - 0.25 * tolerance)
653                && lanes[best].0 - tolerance <= (bbox[0] + bbox[2]) / 2.0
654                && (bbox[0] + bbox[2]) / 2.0
655                    <= lanes[best].1 + tolerance.max(bbox[2] - lanes[best].1);
656            if second >= 0.2 && !fit {
657                None
658            } else if fit
659                || if nested.is_some() {
660                    first >= 0.5
661                } else {
662                    lanes.len() == 1
663                }
664            {
665                Some(best)
666            } else {
667                None
668            }
669        })
670        .collect()
671}
672
673/// 完整局部分式几何按原规则和字符顺序扫描,阈值、重叠、上下侧比较均保留原运算顺序。
674pub fn fraction_members(chars: &[([f64; 4], bool)], rules: &[[f64; 4]], scale: f64) -> Vec<usize> {
675    let mut members = vec![false; chars.len()];
676    for rule in rules {
677        let width = rule[2] - rule[0];
678        let height = rule[3] - rule[1];
679        if width < 2.0f64.max(0.45 * scale)
680            || width > 12.0 * scale
681            || height > 1.25f64.max(0.25 * scale)
682            || width > 8.0 * scale
683        {
684            continue;
685        }
686        let y = (rule[1] + rule[3]) / 2.0;
687        let mut above = Vec::new();
688        let mut below = Vec::new();
689        for (i, &(b, alnum)) in chars.iter().enumerate() {
690            if !alnum || ((b[1] + b[3]) / 2.0 - y).abs() > 2.25 * scale {
691                continue;
692            }
693            let overlap = 0.0f64.max(b[2].min(rule[2]) - b[0].max(rule[0]));
694            if !(overlap > 0.0
695                || rule[0] - 0.25 * scale <= (b[0] + b[2]) / 2.0
696                    && (b[0] + b[2]) / 2.0 <= rule[2] + 0.25 * scale)
697            {
698                continue;
699            }
700            if b[3] <= y + 0.2 * scale && y - b[3] <= 1.75 * scale {
701                above.push(i);
702            }
703            if b[1] >= y - 0.2 * scale && b[1] - y <= 1.75 * scale {
704                below.push(i);
705            }
706        }
707        if !above.is_empty() && !below.is_empty() {
708            for i in above.into_iter().chain(below) {
709                members[i] = true;
710            }
711        }
712    }
713    members
714        .into_iter()
715        .enumerate()
716        .filter_map(|(i, v)| v.then_some(i))
717        .collect()
718}