File size: 2,974 Bytes
0e7a159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6efeb11
 
 
 
0e7a159
 
 
 
 
 
 
 
605885f
 
 
0e7a159
 
 
 
 
 
 
 
 
 
 
 
 
 
4d4cb57
 
0e7a159
 
 
 
1d0edc9
 
 
0e7a159
 
 
 
 
 
4d4cb57
0e7a159
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, Field


RelName = str
Quantifier = Literal["any", "all", "none", "exactly"]
ColorBy = Literal["relation", "family", "confidence", "depth", "macroarea"]
ViewName = Literal["tree", "radial", "map", "table", "stats"]


class WalsClause(BaseModel):
    feature_id: str
    values: list[int] = Field(default_factory=list)


class NodePredicate(BaseModel):
    languages: list[str] = Field(default_factory=list)
    families: list[str] = Field(default_factory=list)
    macroareas: list[str] = Field(default_factory=list)
    statuses: list[str] = Field(default_factory=list)
    term_contains: str = ""
    term_regex: str | None = None
    require_coords: bool = False
    min_lat: float | None = None
    max_lat: float | None = None
    min_lon: float | None = None
    max_lon: float | None = None
    wals: list[WalsClause] = Field(default_factory=list)
    phonemes_have: list[str] = Field(default_factory=list)
    phonemes_lack: list[str] = Field(default_factory=list)
    require_tone: bool | None = None
    # Popularity within language (modern lects). Zipf ~3 rare … ~7 very common.
    min_zipf: float | None = Field(default=None, ge=0.0, le=8.0)
    # Keep leaves whose corpus rank is <= N (1 = most frequent). Orders of magnitude presets in UI.
    max_rank: int | None = Field(default=None, ge=1, le=5_000_000)
    keep_unknown: bool = False


class EdgeFilter(BaseModel):
    relations: list[str] = Field(default_factory=list)
    min_confidence: float = 0.7
    max_depth: int = Field(default=3, ge=1, le=8)
    max_visit: int = Field(default=50_000, ge=100, le=200_000)
    # Also walk toward etymons (forward CSR) and place them opposite descendants.
    include_ancestors: bool = True
    max_ancestor_depth: int = Field(default=4, ge=0, le=8)


class PathFilter(BaseModel):
    node: NodePredicate = Field(default_factory=NodePredicate)
    quantifier: Quantifier = "any"
    exactly_k: int = 1
    relations_any: list[str] = Field(default_factory=list)
    relations_none: list[str] = Field(default_factory=list)
    apply_to_root: bool = False


class TreeQuery(BaseModel):
    term: str
    lang: str
    # Editorial etymology id / etymology_number key; omit to auto-resolve.
    ety: str | None = None
    edges: EdgeFilter = Field(default_factory=EdgeFilter)
    leaf: NodePredicate = Field(default_factory=NodePredicate)
    path: PathFilter = Field(default_factory=PathFilter)
    expand: list[str] = Field(default_factory=list)
    # High default = do not collapse children into cluster stubs.
    cluster_threshold: int = Field(default=1_000_000, ge=1, le=1_000_000)
    max_payload: int = Field(default=50_000, ge=50, le=100_000)
    color_by: ColorBy = "relation"


class SuggestHit(BaseModel):
    term: str
    lang: str
    ety: str = ""
    lang_display: str | None = None
    family_name: str | None = None
    child_count: int = 0
    id: int