# 标题位置查找 Bug 技术细节

## 问题现象

```python
title = "第十章 其他资料"
positions = matcher._find_full_title_positions(title, full_text)
# 返回: [4524, 43321] - 这两个位置是正确的

best_pos = matcher._select_best_position(positions, full_text, title)
# 但实际流程中返回的是: 32460 - 错误位置！
```

## 位置详情对比

### 位置 32460（错误）
```python
line_start = full_text.rfind('\n', 0, 32460) + 1  # = 32436
line_end = full_text.find('\n', 32460)              # = 32473
line_text = full_text[32436:32473]
# 结果: 'XXXX 公司 XXX 专项施工方案'

# 关键：这一行根本不包含"第十章"！
"第十章" in line_text  # False
```

### 位置 43321（正确）
```python
line_start = full_text.rfind('\n', 0, 43321) + 1  # = 43308
line_end = full_text.find('\n', 43321)              # = 43336
line_text = full_text[43308:43336]
# 结果: '第十章  其他资料'

"第十章" in line_text  # True
```

## 代码逻辑疑点

### `_find_full_title_positions` 实现

```python
def _find_full_title_positions(self, title: str, text: str) -> List[int]:
    positions = []
    lines = text.split('\n')
    current_pos = 0

    for i, line in enumerate(lines):
        line_clean = self._remove_escape_chars(line)
        line_normalized = self._normalize_title(line_clean)

        if title_normalized in line_normalized:
            pos_in_line = line_normalized.find(title_normalized)
            line_pos = self._find_pattern_in_line(
                title_normalized, line, pos_in_line
            )
            if line_pos >= 0:
                found_pos = current_pos + line_pos  # <-- 问题可能在这里
                positions.append(found_pos)

        current_pos += len(line) + 1  # <-- 或者这里
```

### 问题分析

1. **位置计算不一致**:
   - `current_pos` 是基于原始行 `line` 的累加
   - `line_pos` 是通过 `_find_pattern_in_line(title_normalized, line, pos_in_line)` 计算
   - `pos_in_line` 是在 `line_normalized` 中的位置，但传给 `_find_pattern_in_line` 的是原始 `line`

2. **`_find_pattern_in_line` 可能的 bug**:
   ```python
   def _find_pattern_in_line(self, pattern: str, line: str, normalized_pos: int) -> int:
       # 这个函数需要根据 normalized_pos 在原始 line 中找到对应位置
       # 如果映射逻辑有误，就会返回错误的 line_pos
   ```

3. **字符处理导致偏移**:
   - `_normalize_title` 可能会增删字符（如合并多个空格）
   - `_remove_escape_chars` 会删除字符
   - 这些操作后的位置映射回原始文本时可能出现偏差

## 调试验证

运行以下代码可以验证问题：

```python
import fitz
from core.construction_review.component.doc_worker.utils.title_matcher import TitleMatcher

file_path = 'D:/wx_work/sichuan_luqiao/lu_sgsc_testfile/测试模版-四川路桥专项施工方案框架以及编制说明（2025修订第三版）- v0.2.pdf'

doc = fitz.open(file_path)
full_text = ""
for page in doc:
    full_text += page.get_text()

matcher = TitleMatcher()
title = "第十章 其他资料"

# 1. 查找所有位置
positions = matcher._find_full_title_positions(title, full_text)
print(f"找到的位置: {positions}")  # [4524, 43321]

# 2. 手动检查位置 32460
pos = 32460
line_start = full_text.rfind('\n', 0, pos) + 1
line_end = full_text.find('\n', pos)
line_text = full_text[line_start:line_end]
print(f"位置 {pos} 所在行: {repr(line_text)}")
print(f"是否包含'第十章': {'第十章' in line_text}")

# 3. 验证正确位置 43321
pos = 43321
line_start = full_text.rfind('\n', 0, pos) + 1
line_end = full_text.find('\n', pos)
line_text = full_text[line_start:line_end]
print(f"位置 {pos} 所在行: {repr(line_text)}")
print(f"是否包含'第十章': {'第十章' in line_text}")
```

## 建议修复方向

### 方向 1: 简化位置计算
直接使用正则表达式在原始文本中搜索，避免复杂的位置映射：

```python
def _find_full_title_positions(self, title: str, text: str) -> List[int]:
    import re

    # 构建兼容空格变体的模式
    title_parts = title.split()
    pattern = r'\s*'.join(re.escape(part) for part in title_parts)

    positions = []
    for m in re.finditer(pattern, text):
        # 验证上下文是否真的是标题行
        line_start = text.rfind('\n', 0, m.start()) + 1
        line_end = text.find('\n', m.end())
        line = text[line_start:line_end].strip()

        # 标题应该独占一行或在行首
        if line.startswith(title.strip()) or self._is_line_only_title(line, title):
            positions.append(m.start())

    return positions
```

### 方向 2: 修复位置映射
如果需要保留现有逻辑，修复 `_find_pattern_in_line` 中的位置映射：

```python
def _find_pattern_in_line(self, pattern: str, line: str, normalized_pos: int) -> int:
    """
    在原始行中找到标准化后模式的对应位置。

    关键：需要建立 normalized_line 和原始 line 之间的字符级映射
    """
    # 1. 构建位置映射表
    normalized_line = self._normalize_title(self._remove_escape_chars(line))

    # 2. 验证 normalized_pos 处的文本确实匹配 pattern
    if normalized_line[normalized_pos:normalized_pos+len(pattern)] != pattern:
        return -1

    # 3. 将 normalized_pos 映射回原始行的位置
    # 这需要跟踪每个字符在清理前后的位置变化
    original_pos = self._map_normalized_to_original(line, normalized_pos)

    return original_pos
```

### 方向 3: 使用 TOC 页码限制搜索范围
利用目录信息缩小标题搜索范围，减少误判：

```python
def find_title_positions(self, items, full_text, pages_content, toc_pages):
    # ...
    for item in items:
        title = item['title']

        # 从 TOC 获取该标题应该在的页码
        expected_page = self._get_expected_page_from_toc(title, toc_info)

        if expected_page:
            # 只在对应页面范围内搜索
            search_start = pages_content[expected_page-1]['start_pos']
            search_end = pages_content[expected_page-1]['end_pos']
            search_text = full_text[search_start:search_end]

            positions = self._find_full_title_positions(title, search_text)
            positions = [p + search_start for p in positions]
        else:
            positions = self._find_full_title_positions(title, full_text)
        # ...
```

## 相关文件

- `core/construction_review/component/doc_worker/utils/title_matcher.py`
  - `_find_full_title_positions`
  - `_find_pattern_in_line`
  - `_select_best_position`

- `core/construction_review/component/doc_worker/pdf_worker/text_splitter.py`
  - `_get_toc_boundary_position` (边界保护逻辑)
  - `split_by_hierarchy`
