Migrate all repos into monorepo context folders

Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
      Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
      Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)

Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
This commit is contained in:
2026-06-30 20:39:52 +02:00
parent 2f2b295531
commit a5f8fb49ab
1717 changed files with 447332 additions and 0 deletions
@@ -0,0 +1,207 @@
"""Phone number extraction and normalization for German telephone formats.
Supports:
- +49 international format (e.g., +49 30 12345678, +49-171-1234567)
- 0xxx domestic format (e.g., 030 12345678, 0171-1234567)
- (0xxx) parenthesized area code format (e.g., (030) 12345678, (0171) 1234567)
"""
import re
# Pattern 1: +49 international format
# +49 followed by 9 to 12 digits with optional spaces or hyphens
_INTERNATIONAL_PATTERN = re.compile(
r"\+49[\s\-]?(\d[\d\s\-]{8,14}\d)"
)
# Pattern 2: 0xxx domestic format
# 0 followed by area code (2-5 digits) then subscriber (4-8 digits), with optional separators
_DOMESTIC_PATTERN = re.compile(
r"(?<!\()0(\d{2,5})[\s\-/]?(\d[\d\s\-]{3,9}\d)"
)
# Pattern 3: (0xxx) parenthesized area code format
# (0xxx) followed by subscriber digits with optional separators
_PARENTHESIZED_PATTERN = re.compile(
r"\(0(\d{2,5})\)[\s\-]?(\d[\d\s\-]{3,9}\d)"
)
def extract_phone_number(text: str) -> str | None:
"""Extract the first German phone number from text.
Supports formats:
- +49 xxx xxxx xxxx (international)
- 0xxx xxxxxxx (domestic with area code)
- (0xxx) xxxxxxx (domestic with parenthesized area code)
Returns the raw matched string or None if no match found.
"""
if not text:
return None
# Collect all matches with their start positions to find the first one
matches: list[tuple[int, str]] = []
for m in _INTERNATIONAL_PATTERN.finditer(text):
matches.append((m.start(), m.group(0)))
for m in _PARENTHESIZED_PATTERN.finditer(text):
matches.append((m.start(), m.group(0)))
for m in _DOMESTIC_PATTERN.finditer(text):
# Avoid matching numbers already captured by parenthesized pattern
start = m.start()
raw = m.group(0)
# Check this isn't inside parentheses
if start > 0 and text[start - 1] == "(":
continue
matches.append((start, raw))
if not matches:
return None
# Return the first match by position
matches.sort(key=lambda x: x[0])
return matches[0][1]
def normalize_phone_number(raw: str) -> str:
"""Normalize a phone number by removing separators and standardizing format.
Strips spaces, hyphens, slashes, and parentheses.
Converts domestic format (leading 0) to international (+49) format.
Returns the normalized number string (e.g., "+4930123456").
"""
if not raw:
return raw
# Remove all separators: spaces, hyphens, slashes, parentheses
cleaned = re.sub(r"[\s\-/\(\)]", "", raw)
# Convert domestic (0xxx) to international (+49xxx)
if cleaned.startswith("0") and not cleaned.startswith("+"):
cleaned = "+49" + cleaned[1:]
return cleaned
def parse_phone_number(text: str) -> dict | None:
"""Parse a phone number string into components.
Returns a dict with keys: country_code, area_code, subscriber
or None if the text cannot be parsed as a German phone number.
"""
if not text:
return None
# First try to extract a phone number from the text
raw = extract_phone_number(text)
if raw is None:
# Try parsing the text directly as a phone number
normalized = normalize_phone_number(text)
if not normalized.startswith("+49"):
return None
digits_after_country = normalized[3:]
if not digits_after_country or len(digits_after_country) < 5:
return None
# Use heuristic: area codes are 2-5 digits, rest is subscriber
area_code, subscriber = _split_area_subscriber(digits_after_country)
return {
"country_code": "+49",
"area_code": area_code,
"subscriber": subscriber,
}
# Parse the extracted raw number
# Try international pattern
m = _INTERNATIONAL_PATTERN.match(raw)
if m:
digits = re.sub(r"[\s\-]", "", m.group(1))
area_code, subscriber = _split_area_subscriber(digits)
return {
"country_code": "+49",
"area_code": area_code,
"subscriber": subscriber,
}
# Try parenthesized pattern
m = _PARENTHESIZED_PATTERN.match(raw)
if m:
area_code = m.group(1)
subscriber = re.sub(r"[\s\-]", "", m.group(2))
return {
"country_code": "+49",
"area_code": area_code,
"subscriber": subscriber,
}
# Try domestic pattern
m = _DOMESTIC_PATTERN.match(raw)
if m:
area_code = m.group(1)
subscriber = re.sub(r"[\s\-]", "", m.group(2))
return {
"country_code": "+49",
"area_code": area_code,
"subscriber": subscriber,
}
return None
def _split_area_subscriber(digits: str) -> tuple[str, str]:
"""Split a digit string into area code and subscriber number.
Uses known German area code lengths:
- 2 digits: major cities (30=Berlin, 40=Hamburg, etc.)
- 3 digits: large cities (211=Düsseldorf, 221=Köln, etc.)
- 4 digits: medium cities and mobile prefixes (171, 172, etc.)
- 5 digits: smaller areas
Heuristic: if total digits <= 10, use shorter area code.
"""
# Known 2-digit area codes (major cities)
two_digit_codes = {"30", "40", "69", "89"}
# Known 3-digit mobile prefixes
three_digit_mobile = {
"151", "152", "153", "155", "156", "157", "159",
"160", "162", "163", "170", "171", "172", "173",
"174", "175", "176", "177", "178", "179",
}
# Check 2-digit area codes
if len(digits) >= 4 and digits[:2] in two_digit_codes:
return digits[:2], digits[2:]
# Check 3-digit codes (mobile and city)
if len(digits) >= 5 and digits[:3] in three_digit_mobile:
return digits[:3], digits[3:]
# Check common 3-digit city codes
three_digit_city = {
"211", "212", "221", "228", "231", "241", "251",
"261", "271", "281", "291",
"311", "321", "331", "341", "351", "361", "371", "381", "391",
"421", "431", "441", "451", "461", "471", "481",
"511", "521", "531", "541", "551", "561", "571", "581", "591",
"611", "621", "631", "641", "651", "661", "671", "681",
"711", "721", "731", "741", "751", "761", "771",
"811", "821", "831", "841", "851", "861", "871",
"911", "921", "931", "941", "951", "961", "971", "981", "991",
}
if len(digits) >= 5 and digits[:3] in three_digit_city:
return digits[:3], digits[3:]
# Default heuristic based on total length
if len(digits) <= 8:
# Short number: assume 3-digit area code
return digits[:3], digits[3:]
elif len(digits) <= 10:
# Medium: assume 4-digit area code
return digits[:4], digits[4:]
else:
# Long: assume 4-digit area code
return digits[:4], digits[4:]