# core/parser.py — Itinerary text parsing: detect cities, transport modes, legs import re from typing import List from models.city import DB, _norm CONNECT = re.compile(r"\s*(?:–|—|-|→|->|>|to)\s*", re.I) PLANE_ANY_RE = re.compile(r"\b(?:vol|flight|plane|fly)\b(?:\s*(?:pour|to|vers|à|a))?\s*([A-Za-zÀ-ỹ' \-/]{2,})", re.I) PLANE_TAIL_RE = re.compile(r"(?:^|[&;,]|-)\\s*(?:vol|flight|plane|fly)\b[^\w]*(?:to|pour|vers|à|a)\s+([A-Za-zÀ-ỹ' \-]+)", re.I) ARRIVE_RE = re.compile(r"\b(arriv(?:ee|ée|e)?|arrival)\b", re.I) DEPART_RE = re.compile(r"\b(d[ée]part|departure|return flight|vol de retour|vol depart|vol départ)\b", re.I) TRAIN_KEYS = {"train", "rail", "tàu", "tau"} BOAT_KEYS = {"boat", "cruise", "croisiere", "croisière", "ferry", "sampan", "barque", "kayak"} def find_cities_in_text(text: str) -> List[str]: return DB.match_in_text(text) def _clean_dest_token(s: str) -> str: s = s.strip() s = re.split(r"[.,;/()|]|(?:\s+-\s+)|(?:—)|(?:–)", s)[0].strip() return s def parse_itinerary(text: str): legs = [] last_for_flight = None arrive_hint = None depart_hint = None for raw in (text or "").splitlines(): line = raw.strip() if not line: continue if re.match(r"^\d{1,2}\s+[A-Za-z]{3}\s+\d{2,4}$", line): continue if ARRIVE_RE.search(line): cs = find_cities_in_text(line) if cs: arrive_hint = cs[0] if DEPART_RE.search(line): cs = find_cities_in_text(line) if cs: depart_hint = cs[-1] for m in PLANE_ANY_RE.finditer(line): dest_raw = _clean_dest_token(m.group(1)) dests = find_cities_in_text(dest_raw) dest = dests[0] if dests else None srcs = find_cities_in_text(line[:m.start()]) src = srcs[-1] if srcs else last_for_flight if src and dest and src != dest: legs.append({"a": src, "b": dest, "mode": "plane"}) last_for_flight = dest def _remove_plane(seg: str) -> str: out = seg for m in PLANE_ANY_RE.finditer(seg): s, e = m.span() out = out[:s] + " " * (e - s) + out[e:] return out base_no_plane = _remove_plane(line) base_can = _norm(base_no_plane) mode = "car" if any(k in base_can for k in TRAIN_KEYS): mode = "train" elif any(k in base_can for k in BOAT_KEYS): mode = "boat" tokens = [t for t in CONNECT.split(base_no_plane) if t.strip()] cities = [] for tk in tokens: found = find_cities_in_text(tk) if found: cities.extend(found) if len(cities) >= 2: for i in range(len(cities) - 1): a, b = cities[i], cities[i + 1] if a == b: continue if any(e["a"] == a and e["b"] == b and e["mode"] == "plane" for e in legs): continue legs.append({"a": a, "b": b, "mode": mode}) last_for_flight = cities[-1] elif len(cities) == 1: last_for_flight = cities[-1] for m in PLANE_TAIL_RE.finditer(line): dest_raw = m.group(1).strip() dests = find_cities_in_text(dest_raw) dest = dests[0] if dests else None if dest and last_for_flight and last_for_flight != dest: if not any(e["a"] == last_for_flight and e["b"] == dest and e["mode"] == "plane" for e in legs): legs.append({"a": last_for_flight, "b": dest, "mode": "plane"}) last_for_flight = dest legs = [e for e in legs if e["a"] != e["b"]] markers = {"arrive_city": arrive_hint, "depart_city": depart_hint} return legs, markers