From upload to node: type detection, extractor dispatch, splitting and the batch ID bug.
detect_file_type() maps extensions to types. EXTRACTORS dict maps types to functions. Each extractor returns a dict with source, type, full_text and optional split data.
_has_parts() checks if extracted data has enough parts. Sections needs 2+ headings, pages needs 2+ pages. Otherwise fallback to single. Max 20 nodes per split.
Per split node: duplicate check, then generate_id with in-memory index (bug fix!), then save. Index written to disk only at the end.