fix(outages): parse portal address at the boundary; strip district suffix

The outages portal appends the district in parentheses, e.g.
"Медкадры улица, 1 (Заельцовский район)". That suffix hid the house number
from the parser, so every portal address failed house matching and /v1/outages
always returned empty.

- address: NormalizeText drops a trailing "(...)" group (root fix, benefits
  every source)
- outages: introduce HouseUnit; the ArcGIS adapter parses the (cleaned) address
  at the boundary and stores the structured unit; MatchingHouse matches on the
  unit (with a parse fallback for sources that only provide text)
- config: portal.url is now optional (core runs without the events module)
- httpapi: always return "outages": [] (no more omitempty); report
  "detail" when the events module is not configured
- tests: regression coverage for the portal district format
This commit is contained in:
Maksim Totmin
2026-10-09 22:05:18 +07:00
parent 59a8b2c164
commit 50f180ea10
10 changed files with 156 additions and 29 deletions
+4 -4
View File
@@ -32,7 +32,8 @@ type ResolveConfig struct {
MaxCandidates int `json:"max_candidates"`
}
// PortalConfig configures the external outages portal (ArcGIS).
// PortalConfig configures the external outages portal (ArcGIS). When URL is
// empty the "events by house" module is disabled and only /v1/resolve works.
type PortalConfig struct {
URL string `json:"url"`
TimeoutSeconds int `json:"timeout_seconds"`
@@ -116,9 +117,8 @@ func (c Config) validate() error {
if c.LLM.Enabled && c.LLM.URL == "" {
return fmt.Errorf("config: llm.url is required when llm.enabled is true")
}
if c.Portal.URL == "" {
return fmt.Errorf("config: portal.url is required")
}
// portal.url is optional: without it the "events by house" module is
// disabled and only /v1/resolve is served.
return nil
}
+4 -3
View File
@@ -60,10 +60,11 @@ func TestLoadMissingSystemPromptFileFailsClosed(t *testing.T) {
}
}
func TestLoadRequiresPortalURL(t *testing.T) {
func TestPortalURLOptional(t *testing.T) {
dir := t.TempDir()
// No portal.url: the events module is disabled, the core still loads.
path := writeFile(t, dir, "config.json", `{"addr": ":6171", "data_path": "x.db"}`)
if _, err := Load(path); err == nil {
t.Fatal("expected error for missing portal.url")
if _, err := Load(path); err != nil {
t.Fatalf("portal.url must be optional: %v", err)
}
}