// Package search — поиск по каталогу через Meilisearch (docs/library-and-storage.md#поиск). package search import ( "strings" "unicode" ) // Established spellings cannot always be derived letter by letter. Keep these // as whole-word alternatives, retaining the mechanical transliteration too. var pronunciationWords = map[string]string{"boulevard": "бульвар"} func TranslitVariants(s string) []string { out := []string{} if mechanical := Translit(s); mechanical != "" { out = append(out, mechanical) } words := strings.Fields(strings.ToLower(s)) changed := false for i, word := range words { if spoken, ok := pronunciationWords[word]; ok { words[i], changed = spoken, true } else if translated := Translit(word); translated != "" { words[i] = translated } } if changed { out = append(out, strings.Join(words, " ")) } return out } func searchKey(s string) string { return strings.Join(strings.FieldsFunc(strings.ToLower(s), func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsDigit(r) }), " ") } // Кириллица → латиница (упрощённая ГОСТ-подобная схема: «Кино» → «kino», «Щедрин» → «shchedrin»). var cyrToLat = map[rune]string{ 'а': "a", 'б': "b", 'в': "v", 'г': "g", 'д': "d", 'е': "e", 'ё': "e", 'ж': "zh", 'з': "z", 'и': "i", 'й': "y", 'к': "k", 'л': "l", 'м': "m", 'н': "n", 'о': "o", 'п': "p", 'р': "r", 'с': "s", 'т': "t", 'у': "u", 'ф': "f", 'х': "kh", 'ц': "ts", 'ч': "ch", 'ш': "sh", 'щ': "shch", 'ъ': "", 'ы': "y", 'ь': "", 'э': "e", 'ю': "yu", 'я': "ya", 'і': "i", 'ї': "yi", 'є': "ye", 'ґ': "g", } // Латиница → кириллица: сначала длинные сочетания. var latToCyr = []struct{ lat, cyr string }{ {"shch", "щ"}, {"zh", "ж"}, {"kh", "х"}, {"ts", "ц"}, {"ch", "ч"}, {"sh", "ш"}, {"yu", "ю"}, {"ya", "я"}, {"yo", "ё"}, {"a", "а"}, {"b", "б"}, {"v", "в"}, {"g", "г"}, {"d", "д"}, {"e", "е"}, {"z", "з"}, {"i", "и"}, {"y", "й"}, {"k", "к"}, {"l", "л"}, {"m", "м"}, {"n", "н"}, {"o", "о"}, {"p", "п"}, {"r", "р"}, {"s", "с"}, {"t", "т"}, {"u", "у"}, {"f", "ф"}, {"h", "х"}, {"c", "к"}, {"w", "в"}, {"x", "кс"}, {"q", "к"}, {"j", "дж"}, } // Translit — вариант строки в другой письменности для поля *_translit: кириллица → латиница, // латиница → кириллица. Смешанные строки переводятся посимвольно. Пустая строка — перевод не нужен. func Translit(s string) string { s = strings.ToLower(s) hasCyr, hasLat := false, false for _, r := range s { switch { case unicode.Is(unicode.Cyrillic, r): hasCyr = true case r < unicode.MaxASCII && unicode.IsLetter(r): hasLat = true } } var b strings.Builder switch { case hasCyr: for _, r := range s { if v, ok := cyrToLat[r]; ok { b.WriteString(v) } else { b.WriteRune(r) } } case hasLat: for i := 0; i < len(s); { matched := false for _, p := range latToCyr { if strings.HasPrefix(s[i:], p.lat) { b.WriteString(p.cyr) i += len(p.lat) matched = true break } } if !matched { b.WriteByte(s[i]) i++ } } default: return "" } return b.String() } // Раскладки ЙЦУКЕН ↔ QWERTY: та же клавиша. const ( qwerty = "`qwertyuiop[]asdfghjkl;'zxcvbnm,./~QWERTYUIOP{}ASDFGHJKL:\"ZXCVBNM<>?" jcuken = "ёйцукенгшщзхъфывапролджэячсмитьбю.ЁЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ," ) var ( toRu = map[rune]rune{} toEn = map[rune]rune{} ) func init() { q, j := []rune(qwerty), []rune(jcuken) for i := range q { toRu[q[i]] = j[i] toEn[j[i]] = q[i] } } // SwitchLayout — запрос, набранный не в той раскладке: «ghbdtn» → «привет», «руддщ» → «hello». // Направление выбирается по большинству букв. Пустая строка — менять нечего. func SwitchLayout(s string) string { lat, cyr := 0, 0 for _, r := range s { switch { case unicode.Is(unicode.Cyrillic, r): cyr++ case r < unicode.MaxASCII && unicode.IsLetter(r): lat++ } } if lat == 0 && cyr == 0 { return "" } m := toRu if cyr > lat { m = toEn } var b strings.Builder for _, r := range s { if v, ok := m[r]; ok { b.WriteRune(v) } else { b.WriteRune(r) } } out := b.String() if out == s { return "" } return out }