package address import ( "regexp" "sort" "strings" ) // The house block is parsed from the end of the string, component by // component, from the outermost (structure) to the innermost (house). Working // right-to-left is what lets us tell a genuine house number apart from a digit // that is part of the street name (e.g. "2-я Станционная улица 1/1 стр."). var ( // "... стр." / "... строение 3" / "... сооружение" / "... с 2" reStructure = regexp.MustCompile(`(?:^|\s)(?:стр|строение|сооружение|с)\.?\s*(\d+)?$`) // "... к9/1" / "... корпус 2а" / "... кД" / "... к 1/4" reBuilding = regexp.MustCompile(`(?:^|\s)(?:к|корп|корпус)\.?\s*(\d*)\s*([а-я])?(?:/(\d+)\s*([а-я])?)?$`) // "... 1/1" / "... 33а" / "... 12 б" / "... 45а/4" / "... 1/1а" reHouse = regexp.MustCompile(`(?:^|\s)(\d+)\s*([а-я])?(?:/(\d+)\s*([а-я])?)?$`) // "2-я", "10-й", "3-е" reOrdinalToken = regexp.MustCompile(`^(\d+)-[а-я]{1,3}$`) ) // Parse decomposes a free-form address into its structured components. It is // total: unparseable input still yields a non-nil ParsedAddress whose street // part is best-effort and whose HasHouse is false. func Parse(raw string) ParsedAddress { normalized := NormalizeText(raw) p := ParsedAddress{Raw: raw, Normalized: normalized} rest := normalized if g, start, ok := matchSuffix(reStructure, rest); ok { p.Structure = trimLeadingZeros(g[1]) rest = strings.TrimSpace(rest[:start]) } if g, start, ok := matchSuffix(reBuilding, rest); ok { // The regex also matches a bare "к"; only accept it when a digit or a // letter actually follows, otherwise it is not a corpus marker. if g[1] != "" || g[2] != "" { p.Building = trimLeadingZeros(g[1]) p.BuildingLetter = firstNonEmpty(g[2], g[4]) p.BuildingFrac = trimLeadingZeros(g[3]) rest = strings.TrimSpace(rest[:start]) } } if g, start, ok := matchSuffix(reHouse, rest); ok { p.House = trimLeadingZeros(g[1]) p.HouseLetter = firstNonEmpty(g[2], g[4]) p.HouseFrac = trimLeadingZeros(g[3]) p.HasHouse = true rest = strings.TrimSpace(rest[:start]) } parseStreet(&p, rest) return p } // firstNonEmpty returns the first non-empty argument. House/building letters // can appear either before or after the fraction ("45а/4" vs "1/1а"). func firstNonEmpty(values ...string) string { for _, v := range values { if v != "" { return v } } return "" } // parseStreet fills the street-related fields of p from the leftover string // (the address with its house block removed). func parseStreet(p *ParsedAddress, s string) { tokens := strings.Fields(s) if len(tokens) == 0 { p.StreetKey = streetKey("", "") return } name := make([]string, 0, len(tokens)) typeFound := false for _, tok := range tokens { // Only the first recognised type token is treated as the type; a // second occurrence is part of the name. if !typeFound { if t, ok := streetTypes[tok]; ok { p.StreetType = t typeFound = true continue } } name = append(name, tok) } name = extractOrdinal(p, name) p.NameDisplay = strings.Join(name, " ") sorted := append([]string(nil), name...) sort.Strings(sorted) p.NameKey = strings.Join(sorted, " ") p.StreetKey = streetKey(p.Ordinal, p.NameKey) } // extractOrdinal recognises a leading ordinal ("2-я", "2 я", "вторая") and // removes it from the name tokens. func extractOrdinal(p *ParsedAddress, tokens []string) []string { if len(tokens) == 0 { return tokens } if m := reOrdinalToken.FindStringSubmatch(tokens[0]); m != nil { p.Ordinal = normalizeOrdinal(m[1]) return tokens[1:] } if isDigits(tokens[0]) && len(tokens) >= 2 && ordinalSuffixes[tokens[1]] { p.Ordinal = normalizeOrdinal(tokens[0]) return tokens[2:] } if d, ok := ordinalWords[tokens[0]]; ok { p.Ordinal = normalizeOrdinal(d) return tokens[1:] } return tokens } // matchSuffix returns the capture groups of the match of re that ends exactly // at the end of s, together with the byte offset where it starts. Go's regexp // is leftmost-first, so we scan all matches and pick the right-anchored one. func matchSuffix(re *regexp.Regexp, s string) (groups []string, start int, ok bool) { for _, loc := range re.FindAllStringSubmatchIndex(s, -1) { if loc[1] != len(s) { continue } n := len(loc) / 2 groups = make([]string, n) for i := 0; i < n; i++ { if loc[2*i] >= 0 { groups[i] = s[loc[2*i]:loc[2*i+1]] } } return groups, loc[0], true } return nil, 0, false }