Skip to main content

docvortex_core/
table_candidates.rs

1//! 表格走廊的连续候选计算:闭区间分配、文本证据及核心几何共享一次准备。
2use crate::row_geometry::RowGeometry;
3use std::collections::HashMap;
4
5pub struct RuleCandidates {
6    centers: Vec<f64>,
7    bands: Vec<usize>,
8    on_rule: Vec<bool>,
9    fragment_counts: Vec<usize>,
10    boxes: Vec<[f64; 4]>,
11    row_centers: Vec<f64>,
12    members: Vec<usize>,
13    member_offsets: Vec<usize>,
14    source_ids: Vec<i64>,
15    geometry: RowGeometry,
16}
17
18impl RuleCandidates {
19    /// 一次验证并保存有限纯数值走廊;横线必须严格递增,不删除或重排任何行。
20    pub fn new(
21        centers: Vec<f64>,
22        rows: Vec<(f64, usize, [f64; 4], Vec<i64>)>,
23    ) -> Result<Self, &'static str> {
24        if centers.iter().any(|v| !v.is_finite())
25            || centers.windows(2).any(|pair| pair[0] >= pair[1])
26            || rows
27                .iter()
28                .any(|row| !row.0.is_finite() || row.2.iter().any(|v| !v.is_finite()))
29        {
30            return Err("nonfinite or unordered rule candidate geometry");
31        }
32        let mut bands = Vec::with_capacity(rows.len());
33        let mut on_rule = Vec::with_capacity(rows.len());
34        let mut fragment_counts = Vec::with_capacity(rows.len());
35        let mut boxes = Vec::with_capacity(rows.len());
36        let mut row_centers = Vec::with_capacity(rows.len());
37        let mut members = Vec::new();
38        let mut member_offsets = Vec::with_capacity(rows.len() + 1);
39        let mut source_ids = Vec::new();
40        let mut interned = HashMap::new();
41        member_offsets.push(0);
42        for (center, count, bbox, sources) in rows {
43            row_centers.push(center);
44            let band = centers.partition_point(|value| *value < center);
45            bands.push(band);
46            on_rule.push(centers.get(band) == Some(&center));
47            fragment_counts.push(count);
48            boxes.push(bbox);
49            // 仅准备时散列原始来源;每个候选按紧凑 ID 位图去重,负数和稀疏 ID 不作下标。
50            for source in sources {
51                let id = *interned.entry(source).or_insert_with(|| {
52                    let id = source_ids.len();
53                    source_ids.push(source);
54                    id
55                });
56                members.push(id);
57            }
58            member_offsets.push(members.len());
59        }
60        let geometry = RowGeometry::new(boxes.clone()).ok_or("invalid row geometry")?;
61        Ok(Self {
62            centers,
63            bands,
64            on_rule,
65            fragment_counts,
66            boxes,
67            row_centers,
68            members,
69            member_offsets,
70            source_ids,
71            geometry,
72        })
73    }
74
75    /// 按原横线中心完整核验短区间条件,不改变任何列统计或候选认领结果。
76    pub fn short_intervals(
77        &self,
78        first: usize,
79        count: usize,
80        height: f64,
81    ) -> Result<bool, &'static str> {
82        if first
83            .checked_add(count)
84            .is_none_or(|end| end > self.centers.len())
85            || !height.is_finite()
86        {
87            return Err("invalid interval height range");
88        }
89        Ok(count >= 2
90            && self.centers[first..first + count]
91                .windows(2)
92                .all(|pair| pair[1] - pair[0] <= 6.0 * height))
93    }
94
95    /// 同一不可变走廊内稳定按行中心排序并分段,所有成员仍保留且不缓存最终候选。
96    pub fn segments(
97        &self,
98        start: usize,
99        end: usize,
100        height: f64,
101    ) -> Result<Vec<Vec<usize>>, &'static str> {
102        if start > end || end > self.boxes.len() || !height.is_finite() {
103            return Err("invalid row segment range");
104        }
105        let mut ordered: Vec<_> = (start..end).collect();
106        ordered.sort_by(|a, b| {
107            self.row_centers[*a]
108                .partial_cmp(&self.row_centers[*b])
109                .unwrap()
110        });
111        let mut output: Vec<Vec<usize>> = Vec::new();
112        for index in ordered {
113            if output.last().is_none_or(|group| {
114                (self.boxes[index][1] - self.boxes[*group.last().unwrap()][3]).max(0.0)
115                    > 3.0 * height
116            }) {
117                output.push(vec![index]);
118            } else {
119                output.last_mut().unwrap().push(index);
120            }
121        }
122        Ok(output)
123    }
124
125    /// 保留共享横线两侧的双归属,在同一行扫描中收集多单元证据,不调用 Python。
126    pub fn partition(
127        &self,
128        start: usize,
129        end: usize,
130        first: usize,
131        rule_count: usize,
132        height: f64,
133    ) -> Result<(Vec<Vec<usize>>, bool), &'static str> {
134        if start > end
135            || end > self.bands.len()
136            || first
137                .checked_add(rule_count)
138                .is_none_or(|last| last > self.centers.len())
139            || !height.is_finite()
140        {
141            return Err("invalid rule candidate interval");
142        }
143        let count = rule_count.saturating_sub(1);
144        let mut groups = vec![Vec::new(); count];
145        let mut dense = vec![false; count];
146        for row in start..end {
147            let band = self.bands[row];
148            if band > first && band - first - 1 < count {
149                let local = band - first - 1;
150                groups[local].push(row);
151                dense[local] |= self.fragment_counts[row] >= 2;
152            }
153            if self.on_rule[row] && band >= first && band - first < count {
154                let local = band - first;
155                groups[local].push(row);
156                dense[local] |= self.fragment_counts[row] >= 2;
157            }
158        }
159        let accepted = rule_count >= 2
160            && (0..count).all(|i| {
161                dense[i]
162                    || (i == 0
163                        && !groups[i].is_empty()
164                        && self.centers[first + 1] - self.centers[first] <= 2.5 * height)
165            });
166        Ok((groups, accepted))
167    }
168
169    /// 按首次出现顺序收集成员,并返回核心并集四个坐标的来源,保留 Python 坐标对象与负零。
170    pub fn core(
171        &self,
172        start: usize,
173        end: usize,
174        rules: &[[f64; 4]],
175    ) -> Result<(Vec<i64>, [usize; 4]), &'static str> {
176        if start >= end
177            || end > self.boxes.len()
178            || rules.is_empty()
179            || rules.iter().flatten().any(|v| !v.is_finite())
180        {
181            return Err("invalid rule candidate core");
182        }
183        let selected = &self.members[self.member_offsets[start]..self.member_offsets[end]];
184        // 位图只属于本次查询;不可变快照无需共享标记或互斥,也不会污染并发查询。
185        let mut seen = vec![0_u64; self.source_ids.len().div_ceil(64)];
186        let mut members = Vec::with_capacity(selected.len().min(self.source_ids.len()));
187        for &id in selected {
188            let word = id / 64;
189            let bit = 1_u64 << (id % 64);
190            if seen[word] & bit == 0 {
191                seen[word] |= bit;
192                members.push(self.source_ids[id]);
193            }
194        }
195        let mut sources = [0; 4];
196        for index in 1..rules.len() {
197            for axis in 0..4 {
198                if better(rules[index][axis], rules[sources[axis]][axis], axis) {
199                    sources[axis] = index;
200                }
201            }
202        }
203        let rows = self
204            .geometry
205            .union_indices(start, end)
206            .ok_or("empty candidate core")?;
207        for axis in 0..4 {
208            if better(
209                self.boxes[rows[axis]][axis],
210                rules[sources[axis]][axis],
211                axis,
212            ) {
213                sources[axis] = rules.len() + rows[axis];
214            }
215        }
216        Ok((members, sources))
217    }
218}
219
220/// 严格比较保持 Python min/max 的首个相等值,不使用改变负零来源的浮点 min/max。
221fn better(candidate: f64, current: f64, axis: usize) -> bool {
222    if axis < 2 {
223        candidate < current
224    } else {
225        candidate > current
226    }
227}
228
229/// 按原顺序筛选、去重和首个匹配分组,统计重复填充行带;不缓存最终表格认领。
230pub fn repeated_fill_bands(boxes: &[[f64; 4]], rule: [f64; 4], em: f64) -> usize {
231    let minimum_width = (8.0 * em).max(0.3 * (rule[2] - rule[0]));
232    let mut candidates: Vec<[f64; 4]> = Vec::new();
233    for &b in boxes {
234        let width = b[2] - b[0];
235        let height = b[3] - b[1];
236        let center = (b[1] + b[3]) / 2.0;
237        let overlap = 0.0_f64.max(b[2].min(rule[2]) - b[0].max(rule[0]));
238        let shorter = width.min(rule[2] - rule[0]);
239        let ratio = if shorter > 0.0 {
240            overlap / shorter
241        } else {
242            0.0
243        };
244        if width < minimum_width
245            || !(0.25 * em <= height && height <= 3.0 * em)
246            || center < rule[1]
247            || center > rule[3]
248            || ratio < 0.8
249        {
250            continue;
251        }
252        if candidates.iter().any(|a| {
253            let width = 0.0_f64.max(b[2].min(a[2]) - b[0].max(a[0]));
254            let height = 0.0_f64.max(b[3].min(a[3]) - b[1].max(a[1]));
255            let smaller = ((b[2] - b[0]) * (b[3] - b[1])).min((a[2] - a[0]) * (a[3] - a[1]));
256            smaller > 0.0 && width * height / smaller >= 0.9
257        }) {
258            continue;
259        }
260        candidates.push(b);
261    }
262    let tolerance = 3.0_f64.max(em);
263    let mut groups: Vec<([f64; 4], usize)> = Vec::new();
264    for b in candidates {
265        let target = groups.iter_mut().find(|(a, _)| {
266            (b[0] - a[0]).abs() <= tolerance
267                && (b[2] - a[2]).abs() <= tolerance
268                && ((b[3] - b[1]) - (a[3] - a[1])).abs() <= tolerance
269        });
270        if let Some((_, count)) = target {
271            *count += 1;
272        } else {
273            groups.push((b, 1));
274        }
275    }
276    groups.iter().map(|(_, count)| *count).max().unwrap_or(0)
277}