PageSourceSearch

https://rusal.ru/local/templates/main/js/jquery.suggestions.js?1763783823162621

js rusal.ru collected 2026-10-02 03:25:03 UTC 162,621 bytes, 5,314 lines download raw bytes

vendor: 157,934 bytes, lines 1-5235
1/**
2 * DaData.ru Suggestions jQuery plugin, version 22.6.0
3 *
4 * DaData.ru Suggestions jQuery plugin is freely distributable under the terms of MIT-style license
5 * Built on DevBridge Autocomplete for jQuery (https://github.com/devbridge/jQuery-Autocomplete)
6 * For details, see https://github.com/hflabs/suggestions-jquery
7 */
8(function (global, factory) {
9	typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('jquery')) :
10	typeof define === 'function' && define.amd ? define(['jquery'], factory) :
11	(factory(global.jQuery));
12}(this, (function ($) { 'use strict';
13
14$ = $ && $.hasOwnProperty('default') ? $['default'] : $;
15
16/**
17 * Утилиты для работы с типами.
18 */
19var lang_util = {
20    /**
21     * Проверяет, является ли аргумент массивом.
22     */
23    isArray: function(array) {
24        return Array.isArray(array);
25    },
26
27    /**
28     * Проверяет, является ли аргумент функцией.
29     */
30    isFunction: function(it) {
31        return Object.prototype.toString.call(it) === "[object Function]";
32    },
33
34    /**
35     * Проверяет, является ли аргумент пустым объектом ({}).
36     */
37    isEmptyObject: function(obj) {
38        return Object.keys(obj).length === 0 && obj.constructor === Object;
39    },
40
41    /**
42     * Проверяет, является ли аргумент «обычным» объектом
43     * (не undefiend, не null, не DOM-элемент)
44     */
45    isPlainObject: function(obj) {
46        if (
47            obj === undefined ||
48            typeof obj !== "object" ||
49            obj === null ||
50            obj.nodeType ||
51            obj === obj.window
52        ) {
53            return false;
54        }
55        if (
56            obj.constructor &&
57            !Object.prototype.hasOwnProperty.call(
58                obj.constructor.prototype,
59                "isPrototypeOf"
60            )
61        ) {
62            return false;
63        }
64        return true;
65    }
66};
67
68/**
69 * Утилиты для работы с коллекциями.
70 */
71var collection_util = {
72    /**
73     * Возвращает массив без пустых элементов
74     */
75    compact: function(array) {
76        return array.filter(function(el) {
77            return !!el;
78        });
79    },
80
81    /**
82     * Итерирует по элементам массива или полям объекта.
83     * Ведёт себя как $.each() - прерывает выполнение, если функция-обработчик возвращает false.
84     * @param {Object|Array} obj - массив или объект
85     * @param {eachCallback} callback - функция-обработчик
86     */
87    each: function(obj, callback) {
88        if (Array.isArray(obj)) {
89            obj.some(function(el, idx) {
90                return callback(el, idx) === false;
91            });
92            return;
93        }
94        Object.keys(obj).some(function(key) {
95            var value = obj[key];
96            return callback(value, key) === false;
97        });
98    },
99
100    /**
101     * Пересечение массивов: ([1,2,3,4], [2,4,5,6]) => [2,4]
102     * Исходные массивы не меняются.
103     */
104    intersect: function(array1, array2) {
105        var result = [];
106        if (!Array.isArray(array1) || !Array.isArray(array2)) {
107            return result;
108        }
109        return array1.filter(function(el) {
110            return array2.indexOf(el) !== -1;
111        });
112    },
113
114    /**
115     * Разность массивов: ([1,2,3,4], [2,4,5,6]) => [1,3]
116     * Исходные массивы не меняются.
117     */
118    minus: function(array1, array2) {
119        if (!array2 || array2.length === 0) {
120            return array1;
121        }
122        return array1.filter(function(el) {
123            return array2.indexOf(el) === -1;
124        });
125    },
126
127    /**
128     * Обрачивает переданный объект в массив.
129     * Если передан массив, возвращает его копию.
130     */
131    makeArray: function(arrayLike) {
132        if (lang_util.isArray(arrayLike)) {
133            return Array.prototype.slice.call(arrayLike);
134        } else {
135            return [arrayLike];
136        }
137    },
138
139    /**
140     * Разность массивов с частичным совпадением элементов.
141     * Если элемент второго массива включает в себя элемент первого,
142     * элементы считаются равными.
143     */
144    minusWithPartialMatching: function(array1, array2) {
145        if (!array2 || array2.length === 0) {
146            return array1;
147        }
148        return array1.filter(function(el) {
149            return !array2.some(function(el2) {
150                return el2.indexOf(el) === 0;
151            });
152        });
153    },
154
155    /**
156     * Копирует массив, начиная с указанного элемента.
157     * @param obj - массив
158     * @param start - индекс, начиная с которого надо скопировать
159     */
160    slice: function(obj, start) {
161        return Array.prototype.slice.call(obj, start);
162    }
163};
164
165/**
166 * Утилиты для работы с функциями.
167 */
168var func_util = {
169    /**
170     * Выполняет функцию с указанной задержкой.
171     * @param {Function} handler - функция
172     * @param {number} delay - задержка в миллисекундах
173     */
174    delay: function(handler, delay) {
175        return setTimeout(handler, delay || 0);
176    }
177};
178
179/**
180 * Утилиты для работы с объектами.
181 */
182var object_util = {
183    /**
184     * Сравнивает два объекта по полям, которые присутствуют в обоих
185     * @returns {boolean} true, если поля совпадают, false в противном случае
186     */
187    areSame: function self(a, b) {
188        var same = true;
189
190        if (typeof a != typeof b) {
191            return false;
192        }
193
194        if (typeof a == "object" && a != null && b != null) {
195            collection_util.each(a, function(value, i) {
196                return (same = self(value, b[i]));
197            });
198            return same;
199        }
200
201        return a === b;
202    },
203
204    /**
205     * Копирует свойства и их значения из исходных объектов в целевой
206     */
207    assign: function(target, varArgs) {
208        if (typeof Object.assign === "function") {
209            return Object.assign.apply(null, arguments);
210        }
211        if (target == null) {
212            // TypeError if undefined or null
213            throw new TypeError("Cannot convert undefined or null to object");
214        }
215
216        var to = Object(target);
217
218        for (var index = 1; index < arguments.length; index++) {
219            var nextSource = arguments[index];
220
221            if (nextSource != null) {
222                // Skip over if undefined or null
223                for (var nextKey in nextSource) {
224                    // Avoid bugs when hasOwnProperty is shadowed
225                    if (
226                        Object.prototype.hasOwnProperty.call(
227                            nextSource,
228                            nextKey
229                        )
230                    ) {
231                        to[nextKey] = nextSource[nextKey];
232                    }
233                }
234            }
235        }
236        return to;
237    },
238
239    /**
240     * Клонирует объект глубоким копированием
241     */
242    clone: function(obj) {
243        return JSON.parse(JSON.stringify(obj));
244    },
245
246    /**
247     * Возвращает копию объекта без пустых полей
248     * (без undefined, null и '')
249     * @param obj
250     */
251    compact: function(obj) {
252        var copy = object_util.clone(obj);
253
254        collection_util.each(copy, function(val, key) {
255            if (val === null || val === undefined || val === "") {
256                delete copy[key];
257            }
258        });
259
260        return copy;
261    },
262
263    /**
264     * Проверяет, что указанные поля в объекте заполнены.
265     * @param {Object} obj - проверяемый объект
266     * @param {Array} fields - список названий полей, которые надо проверить
267     * @returns {boolean}
268     */
269    fieldsAreNotEmpty: function(obj, fields) {
270        if (!lang_util.isPlainObject(obj)) {
271            return false;
272        }
273        var result = true;
274        collection_util.each(fields, function(field, i) {
275            result = !!obj[field];
276            return result;
277        });
278        return result;
279    },
280
281    /**
282     * Возвращает вложенное значение по указанному пути
283     * например, 'data.address.value'
284     */
285    getDeepValue: function self(obj, name) {
286        var path = name.split("."),
287            step = path.shift();
288
289        return (
290            obj && (path.length ? self(obj[step], path.join(".")) : obj[step])
291        );
292    },
293
294    /**
295     * Возвращает карту объектов по их идентификаторам.
296     * Принимает на вход массив объектов и идентифицирующее поле.
297     * Возвращает карты, ключом в которой является значение идентифицирующего поля,
298     *   а значением — исходный объект.
299     * Заодно добавляет объектам поле с порядковым номером.
300     * @param {Array} objectsArray - массив объектов
301     * @param {string} idField - название идентифицирующего поля
302     * @param {string} indexField - название поля с порядковым номером
303     * @return {Object} карта объектов по их идентификаторам
304     */
305    indexObjectsById: function(objectsArray, idField, indexField) {
306        var result = {};
307
308        collection_util.each(objectsArray, function(obj, idx) {
309            var key = obj[idField];
310            var val = {};
311
312            if (indexField) {
313                val[indexField] = idx;
314            }
315
316            result[key] = object_util.assign(val, obj);
317        });
318
319        return result;
320    }
321};
322
323var KEYS = {
324    ENTER: 13,
325    ESC: 27,
326    TAB: 9,
327    SPACE: 32,
328    UP: 38,
329    DOWN: 40
330};
331
332var CLASSES = {
333    hint: "suggestions-hint",
334    mobile: "suggestions-mobile",
335    nowrap: "suggestions-nowrap",
336    promo: "suggestions-promo",
337    selected: "suggestions-selected",
338    suggestion: "suggestions-suggestion",
339    subtext: "suggestions-subtext",
340    subtext_inline: "suggestions-subtext suggestions-subtext_inline",
341    subtext_delimiter: "suggestions-subtext-delimiter",
342    subtext_label: "suggestions-subtext suggestions-subtext_label",
343    removeConstraint: "suggestions-remove",
344    value: "suggestions-value"
345};
346
347var EVENT_NS = ".suggestions";
348var DATA_ATTR_KEY = "suggestions";
349var WORD_DELIMITERS = "\\s\"'~\\*\\.,:\\|\\[\\]\\(\\)\\{\\}<>№";
350var WORD_PARTS_DELIMITERS = "\\-\\+\\\\\\?!@#$%^&";
351
352/**
353 * Утилиты для работы с текстом.
354 */
355
356var WORD_SPLITTER = new RegExp("[" + WORD_DELIMITERS + "]+", "g");
357var WORD_PARTS_SPLITTER = new RegExp("[" + WORD_PARTS_DELIMITERS + "]+", "g");
358
359var text_util = {
360    /**
361     * Заменяет амперсанд, угловые скобки и другие подобные символы
362     * на HTML-коды
363     */
364    escapeHtml: function(str) {
365        var map = {
366            "&": "&amp;",
367            "<": "&lt;",
368            ">": "&gt;",
369            '"': "&quot;",
370            "'": "&#x27;",
371            "/": "&#x2F;"
372        };
373
374        if (str) {
375            collection_util.each(map, function(html, ch) {
376                str = str.replace(new RegExp(ch, "g"), html);
377            });
378        }
379        return str;
380    },
381
382    /**
383     * Эскейпирует символы RegExp-шаблона обратным слешем
384     * (для передачи в конструктор регулярных выражений)
385     */
386    escapeRegExChars: function(value) {
387        return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
388    },
389
390    /**
391     * Приводит слово к нижнему регистру и заменяет ё → е
392     */
393    formatToken: function(token) {
394        return token && token.toLowerCase().replace(/[ёЁ]/g, "е");
395    },
396
397    /**
398     * Возвращает регулярное выражение для разбивки строки на слова
399     */
400    getWordExtractorRegExp: function() {
401        return new RegExp(
402            "([^" + WORD_DELIMITERS + "]*)([" + WORD_DELIMITERS + "]*)",
403            "g"
404        );
405    },
406
407    /**
408     * Вырезает из строки стоп-слова
409     */
410    normalize: function(str, stopwords) {
411        return text_util.split(str, stopwords).join(" ");
412    },
413
414    /**
415     * Добивает строку указанным символов справа до указанной длины
416     * @param sourceString  исходная строка
417     * @param targetLength  до какой длины добивать
418     * @param padString  каким символом добивать
419     * @returns строка указанной длины
420     */
421    padEnd: function(sourceString, targetLength, padString) {
422        if (String.prototype.padEnd) {
423            return sourceString.padEnd(targetLength, padString);
424        }
425        targetLength = targetLength >> 0; //floor if number or convert non-number to 0;
426        padString = String(typeof padString !== "undefined" ? padString : " ");
427        if (sourceString.length > targetLength) {
428            return String(sourceString);
429        } else {
430            targetLength = targetLength - sourceString.length;
431            if (targetLength > padString.length) {
432                padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
433            }
434            return String(sourceString) + padString.slice(0, targetLength);
435        }
436    },
437
438    /**
439     * Нормализует строку, разбивает на слова,
440     * отсеивает стоп-слова из списка.
441     * Расклеивает буквы и цифры, написанные слитно.
442     */
443    split: function(str, stopwords) {
444        var cleanStr = str
445            .toLowerCase()
446            .replace("ё", "е")
447            .replace(/(\d+)([а-я]{2,})/g, "$1 $2")
448            .replace(/([а-я]+)(\d+)/g, "$1 $2");
449
450        var words = collection_util.compact(cleanStr.split(WORD_SPLITTER));
451        if (!words.length) {
452            return [];
453        }
454        var lastWord = words.pop();
455        var goodWords = collection_util.minus(words, stopwords);
456        goodWords.push(lastWord);
457        return goodWords;
458    },
459
460    /**
461     * Заменяет слова на составные части.
462     * В отличие от withSubTokens, не сохраняет исходные слова.
463     */
464    splitTokens: function(tokens) {
465        var result = [];
466        collection_util.each(tokens, function(token, i) {
467            var subtokens = token.split(WORD_PARTS_SPLITTER);
468            result = result.concat(collection_util.compact(subtokens));
469        });
470        return result;
471    },
472
473    /**
474     * Проверяет, включает ли строка 1 строку 2.
475     * Если строки равны, возвращает false.
476     */
477    stringEncloses: function(str1, str2) {
478        return (
479            str1.length > str2.length &&
480            str1.toLowerCase().indexOf(str2.toLowerCase()) !== -1
481        );
482    },
483
484    /**
485     * Возвращает список слов из строки.
486     * При этом первыми по порядку идут «предпочтительные» слова
487     * (те, что не входят в список «нежелательных»).
488     * Составные слова тоже разбивает на части.
489     * @param {string} value - строка
490     * @param {Array} unformattableTokens - «нежелательные» слова
491     * @return {Array} Массив атомарных слов
492     */
493    tokenize: function(value, unformattableTokens) {
494        var tokens = collection_util.compact(
495            text_util.formatToken(value).split(WORD_SPLITTER)
496        );
497        // Move unformattableTokens to the end.
498        // This will help to apply them only if no other tokens match
499        var preferredTokens = collection_util.minus(
500            tokens,
501            unformattableTokens
502        );
503        var otherTokens = collection_util.minus(tokens, preferredTokens);
504        tokens = text_util.withSubTokens(preferredTokens.concat(otherTokens));
505        return tokens;
506    },
507
508    /**
509     * Разбивает составные слова на части
510     * и дописывает их к исходному массиву.
511     * @param {Array} tokens - слова
512     * @return {Array} Массив атомарных слов
513     */
514    withSubTokens: function(tokens) {
515        var result = [];
516        collection_util.each(tokens, function(token, i) {
517            var subtokens = token.split(WORD_PARTS_SPLITTER);
518            result.push(token);
519            if (subtokens.length > 1) {
520                result = result.concat(collection_util.compact(subtokens));
521            }
522        });
523        return result;
524    }
525};
526
527/**
528 * jQuery API.
529 */
530var jqapi = {
531    Deferred: function() {
532        return $.Deferred();
533    },
534
535    ajax: function(settings) {
536        return $.ajax(settings);
537    },
538
539    extend: function() {
540        return $.extend.apply(null, arguments);
541    },
542
543    isJqObject: function(obj) {
544        return obj instanceof $;
545    },
546
547    param: function(obj) {
548        return $.param(obj);
549    },
550
551    proxy: function(func, context) {
552        return $.proxy(func, context);
553    },
554
555    select: function(selector) {
556        return $(selector);
557    },
558
559    supportsCors: function() {
560        return $.support.cors;
561    }
562};
563
564/**
565 * Утилиты для работы через AJAX
566 */
567var ajax = {
568    /**
569     * HTTP-метод, который поддерживает браузер
570     */
571    getDefaultType: function() {
572        return jqapi.supportsCors() ? "POST" : "GET";
573    },
574
575    /**
576     * Content-type, который поддерживает браузер
577     */
578    getDefaultContentType: function() {
579        return jqapi.supportsCors()
580            ? "application/json"
581            : "application/x-www-form-urlencoded";
582    },
583
584    /**
585     * Меняет HTTPS на протокол страницы, если браузер не поддерживает CORS
586     */
587    fixURLProtocol: function(url) {
588        return jqapi.supportsCors()
589            ? url
590            : url.replace(/^https?:/, location.protocol);
591    },
592
593    /**
594     * Записывает параметры в GET-строку
595     */
596    addUrlParams: function(url, params) {
597        return url + (/\?/.test(url) ? "&" : "?") + jqapi.param(params);
598    },
599
600    /**
601     * Сериализует объект для передачи по сети.
602     * Либо в JSON-строку (если браузер поддерживает CORS),
603     *   либо в GET-строку.
604     */
605    serialize: function(data) {
606        if (jqapi.supportsCors()) {
607            return JSON.stringify(data, function(key, value) {
608                return value === null ? undefined : value;
609            });
610        } else {
611            data = object_util.compact(data);
612            return jqapi.param(data, true);
613        }
614    }
615};
616
617/**
618 * Возвращает автоинкрементный идентификатор.
619 * @param {string} prefix - префикс для идентификатора
620 */
621var generateId = (function() {
622    var counter = 0;
623    return function(prefix) {
624        return (prefix || "") + ++counter;
625    };
626})();
627
628/**
629 * Утилиты на все случаи жизни.
630 */
631var utils = {
632    escapeRegExChars: text_util.escapeRegExChars,
633    escapeHtml: text_util.escapeHtml,
634    formatToken: text_util.formatToken,
635    normalize: text_util.normalize,
636    reWordExtractor: text_util.getWordExtractorRegExp,
637    stringEncloses: text_util.stringEncloses,
638
639    addUrlParams: ajax.addUrlParams,
640    getDefaultContentType: ajax.getDefaultContentType,
641    getDefaultType: ajax.getDefaultType,
642    fixURLProtocol: ajax.fixURLProtocol,
643    serialize: ajax.serialize,
644
645    arrayMinus: collection_util.minus,
646    arrayMinusWithPartialMatching: collection_util.minusWithPartialMatching,
647    arraysIntersection: collection_util.intersect,
648    compact: collection_util.compact,
649    each: collection_util.each,
650    makeArray: collection_util.makeArray,
651    slice: collection_util.slice,
652
653    delay: func_util.delay,
654
655    areSame: object_util.areSame,
656    compactObject: object_util.compact,
657    getDeepValue: object_util.getDeepValue,
658    fieldsNotEmpty: object_util.fieldsAreNotEmpty,
659    indexBy: object_util.indexObjectsById,
660
661    isArray: lang_util.isArray,
662    isEmptyObject: lang_util.isEmptyObject,
663    isFunction: lang_util.isFunction,
664    isPlainObject: lang_util.isPlainObject,
665
666    uniqueId: generateId
667};
668
669var DEFAULT_OPTIONS = {
670    $helpers: null,
671    autoSelectFirst: false,
672    containerClass: "suggestions-suggestions",
673    count: 5,
674    deferRequestBy: 100,
675    enrichmentEnabled: true,
676    formatResult: null,
677    formatSelected: null,
678    headers: null,
679    hint: "Выберите вариант или продолжите ввод",
680    initializeInterval: 100,
681    language: null,
682    minChars: 1,
683    mobileWidth: 600,
684    noCache: false,
685    noSuggestionsHint: null,
686    onInvalidateSelection: null,
687    onSearchComplete: $.noop,
688    onSearchError: $.noop,
689    onSearchStart: $.noop,
690    onSelect: null,
691    onSelectNothing: null,
692    onSuggestionsFetch: null,
693    paramName: "query",
694    params: {},
695    preventBadQueries: false,
696    requestMode: "suggest",
697    scrollOnFocus: false,
698    // основной url, может быть переопределен
699    serviceUrl: "https://suggestions.dadata.ru/suggestions/api/4_1/rs",
700    tabDisabled: false,
701    timeout: 3000,
702    triggerSelectOnBlur: true,
703    triggerSelectOnEnter: true,
704    triggerSelectOnSpace: false,
705    type: null,
706    // url, который заменяет serviceUrl + method + type
707    // то есть, если он задан, то для всех запросов будет использоваться именно он
708    // если не поддерживается cors то к url будут добавлены параметры ?token=...&version=...
709    // и заменен протокол на протокол текущей страницы
710    url: null
711};
712
713/**
714 * Factory to create same parent checker function
715 * @param preprocessFn called on each value before comparison
716 * @returns {Function} same parent checker function
717 */
718function sameParentChecker(preprocessFn) {
719    return function(suggestions) {
720        if (suggestions.length === 0) {
721            return false;
722        }
723        if (suggestions.length === 1) {
724            return true;
725        }
726
727        var parentValue = preprocessFn(suggestions[0].value),
728            aliens = suggestions.filter(function(suggestion) {
729                return (
730                    preprocessFn(suggestion.value).indexOf(parentValue) !== 0
731                );
732            });
733
734        return aliens.length === 0;
735    };
736}
737
738/**
739 * Default same parent checker. Compares raw values.
740 * @type {Function}
741 */
742var haveSameParent = sameParentChecker(function(val) {
743    return val;
744});
745
746/**
747 * Сравнивает запрос c подсказками, по словам.
748 * Срабатывает, только если у всех подсказок общий родитель
749 * (функция сверки передаётся параметром).
750 * Игнорирует стоп-слова.
751 * Возвращает индекс единственной подходящей подсказки
752 * или -1, если подходящих нет или несколько.
753 */
754function _matchByWords(stopwords, parentCheckerFn) {
755    return function(query, suggestions) {
756        var queryTokens;
757        var matches = [];
758
759        if (parentCheckerFn(suggestions)) {
760            queryTokens = text_util.splitTokens(
761                text_util.split(query, stopwords)
762            );
763
764            collection_util.each(suggestions, function(suggestion, i) {
765                var suggestedValue = suggestion.value;
766
767                if (text_util.stringEncloses(query, suggestedValue)) {
768                    return false;
769                }
770
771                // check if query words are a subset of suggested words
772                var suggestionWords = text_util.splitTokens(
773                    text_util.split(suggestedValue, stopwords)
774                );
775
776                if (
777                    collection_util.minus(queryTokens, suggestionWords)
778                        .length === 0
779                ) {
780                    matches.push(i);
781                }
782            });
783        }
784
785        return matches.length === 1 ? matches[0] : -1;
786    };
787}
788
789/**
790 * Matchers return index of suitable suggestion
791 * Context inside is optionally set in types.js
792 */
793var matchers = {
794    /**
795     * Matches query against suggestions, removing all the stopwords.
796     */
797    matchByNormalizedQuery: function(stopwords) {
798        return function(query, suggestions) {
799            var normalizedQuery = text_util.normalize(query, stopwords);
800            var matches = [];
801
802            collection_util.each(suggestions, function(suggestion, i) {
803                var suggestedValue = suggestion.value.toLowerCase();
804                // if query encloses suggestion, than it has already been selected
805                // so we should not select it anymore
806                if (text_util.stringEncloses(query, suggestedValue)) {
807                    return false;
808                }
809                // if there is suggestion that contains query as its part
810                // than we should ignore all other matches, even full ones
811                if (suggestedValue.indexOf(normalizedQuery) > 0) {
812                    return false;
813                }
814                if (
815                    normalizedQuery ===
816                    text_util.normalize(suggestedValue, stopwords)
817                ) {
818                    matches.push(i);
819                }
820            });
821
822            return matches.length === 1 ? matches[0] : -1;
823        };
824    },
825
826    matchByWords: function(stopwords) {
827        return _matchByWords(stopwords, haveSameParent);
828    },
829
830    matchByWordsAddress: function(stopwords) {
831        return _matchByWords(stopwords, haveSameParent);
832    },
833
834    /**
835     * Matches query against values contained in suggestion fields
836     * for cases, when there is only one suggestion
837     * only considers fields specified in fields map
838     * uses partial matching:
839     *   "0445" vs { value: "ALFA-BANK", data: { "bic": "044525593" }} is a match
840     */
841    matchByFields: function(fields) {
842        return function(query, suggestions) {
843            var tokens = text_util.splitTokens(text_util.split(query));
844            var suggestionWords = [];
845
846            if (suggestions.length === 1) {
847                if (fields) {
848                    collection_util.each(fields, function(stopwords, field) {
849                        var fieldValue = object_util.getDeepValue(
850                            suggestions[0],
851                            field
852                        );
853                        var fieldWords =
854                            fieldValue &&
855                            text_util.splitTokens(
856                                text_util.split(fieldValue, stopwords)
857                            );
858
859                        if (fieldWords && fieldWords.length) {
860                            suggestionWords = suggestionWords.concat(
861                                fieldWords
862                            );
863                        }
864                    });
865                }
866
867                if (
868                    collection_util.minusWithPartialMatching(
869                        tokens,
870                        suggestionWords
871                    ).length === 0
872                ) {
873                    return 0;
874                }
875            }
876
877            return -1;
878        };
879    }
880};
881
882var ADDRESS_STOPWORDS = [
883    "ао",
884    "аобл",
885    "дом",
886    "респ",
887    "а/я",
888    "аал",
889    "автодорога",
890    "аллея",
891    "арбан",
892    "аул",
893    "б-р",
894    "берег",
895    "бугор",
896    "вал",
897    "вл",
898    "волость",
899    "въезд",
900    "высел",
901    "г",
902    "городок",
903    "гск",
904    "д",
905    "двлд",
906    "днп",
907    "дор",
908    "дп",
909    "ж/д_будка",
910    "ж/д_казарм",
911    "ж/д_оп",
912    "ж/д_платф",
913    "ж/д_пост",
914    "ж/д_рзд",
915    "ж/д_ст",
916    "жилзона",
917    "жилрайон",
918    "жт",
919    "заезд",
920    "заимка",
921    "зона",
922    "к",
923    "казарма",
924    "канал",
925    "кв",
926    "кв-л",
927    "км",
928    "кольцо",
929    "комн",
930    "кордон",
931    "коса",
932    "кп",
933    "край",
934    "линия",
935    "лпх",
936    "м",
937    "массив",
938    "местность",
939    "мкр",
940    "мост",
941    "н/п",
942    "наб",
943    "нп",
944    "обл",
945    "округ",
946    "остров",
947    "оф",
948    "п",
949    "п/о",
950    "п/р",
951    "п/ст",
952    "парк",
953    "пгт",
954    "пер",
955    "переезд",
956    "пл",
957    "пл-ка",
958    "платф",
959    "погост",
960    "полустанок",
961    "починок",
962    "пр-кт",
963    "проезд",
964    "промзона",
965    "просек",
966    "просека",
967    "проселок",
968    "проток",
969    "протока",
970    "проулок",
971    "р-н",
972    "рзд",
973    "россия",
974    "рп",
975    "ряды",
976    "с",
977    "с/а",
978    "с/мо",
979    "с/о",
980    "с/п",
981    "с/с",
982    "сад",
983    "сквер",
984    "сл",
985    "снт",
986    "спуск",
987    "ст",
988    "ст-ца",
989    "стр",
990    "тер",
991    "тракт",
992    "туп",
993    "у",
994    "ул",
995    "уч-к",
996    "ф/х",
997    "ферма",
998    "х",
999    "ш",
1000    "бульвар",
1001    "владение",
1002    "выселки",
1003    "гаражно-строительный",
1004    "город",
1005    "деревня",
1006    "домовладение",
1007    "дорога",
1008    "квартал",
1009    "километр",
1010    "комната",
1011    "корпус",
1012    "литер",
1013    "леспромхоз",
1014    "местечко",
1015    "микрорайон",
1016    "набережная",
1017    "область",
1018    "переулок",
1019    "платформа",
1020    "площадка",
1021    "площадь",
1022    "поселение",
1023    "поселок",
1024    "проспект",
1025    "разъезд",
1026    "район",
1027    "республика",
1028    "село",
1029    "сельсовет",
1030    "слобода",
1031    "сооружение",
1032    "станица",
1033    "станция",
1034    "строение",
1035    "территория",
1036    "тупик",
1037    "улица",
1038    "улус",
1039    "участок",
1040    "хутор",
1041    "шоссе"
1042];
1043
1044/**
1045 * Компоненты адреса
1046 * @type {*[]}
1047 * id {String} Наименование типа
1048 * fields {Array of Strings}
1049 * forBounds {Boolean} может использоваться в ограничениях
1050 * forLocations {Boolean}
1051 * kladrFormat {Object}
1052 * fiasType {String} Наименование соответствующего ФИАС типа
1053 */
1054var ADDRESS_COMPONENTS = [
1055    {
1056        id: "kladr_id",
1057        fields: ["kladr_id"],
1058        forBounds: false,
1059        forLocations: true
1060    },
1061    {
1062        id: "postal_code",
1063        fields: ["postal_code"],
1064        forBounds: false,
1065        forLocations: true
1066    },
1067    {
1068        id: "country_iso_code",
1069        fields: ["country_iso_code"],
1070        forBounds: false,
1071        forLocations: true
1072    },
1073    {
1074        id: "country",
1075        fields: ["country"],
1076        forBounds: true,
1077        forLocations: true,
1078        kladrFormat: { digits: 0, zeros: 13 },
1079        fiasType: "country_iso_code"
1080    },
1081    {
1082        id: "region_iso_code",
1083        fields: ["region_iso_code"],
1084        forBounds: false,
1085        forLocations: true
1086    },
1087    {
1088        id: "region_fias_id",
1089        fields: ["region_fias_id"],
1090        forBounds: false,
1091        forLocations: true
1092    },
1093    {
1094        id: "region_type_full",
1095        fields: ["region_type_full"],
1096        forBounds: false,
1097        forLocations: true,
1098        kladrFormat: { digits: 2, zeros: 11 },
1099        fiasType: "region_fias_id"
1100    },
1101    {
1102        id: "region",
1103        fields: [
1104            "region",
1105            "region_type",
1106            "region_type_full",
1107            "region_with_type"
1108        ],
1109        forBounds: true,
1110        forLocations: true,
1111        kladrFormat: { digits: 2, zeros: 11 },
1112        fiasType: "region_fias_id"
1113    },
1114    {
1115        id: "area_fias_id",
1116        fields: ["area_fias_id"],
1117        forBounds: false,
1118        forLocations: true
1119    },
1120    {
1121        id: "area_type_full",
1122        fields: ["area_type_full"],
1123        forBounds: false,
1124        forLocations: true,
1125        kladrFormat: { digits: 5, zeros: 8 },
1126        fiasType: "area_fias_id"
1127    },
1128    {
1129        id: "area",
1130        fields: ["area", "area_type", "area_type_full", "area_with_type"],
1131        forBounds: true,
1132        forLocations: true,
1133        kladrFormat: { digits: 5, zeros: 8 },
1134        fiasType: "area_fias_id"
1135    },
1136    {
1137        id: "city_fias_id",
1138        fields: ["city_fias_id"],
1139        forBounds: false,
1140        forLocations: true
1141    },
1142    {
1143        id: "city_type_full",
1144        fields: ["city_type_full"],
1145        forBounds: false,
1146        forLocations: true,
1147        kladrFormat: { digits: 8, zeros: 5 },
1148        fiasType: "city_fias_id"
1149    },
1150    {
1151        id: "city",
1152        fields: ["city", "city_type", "city_type_full", "city_with_type"],
1153        forBounds: true,
1154        forLocations: true,
1155        kladrFormat: { digits: 8, zeros: 5 },
1156        fiasType: "city_fias_id"
1157    },
1158    {
1159        id: "city_district_fias_id",
1160        fields: ["city_district_fias_id"],
1161        forBounds: false,
1162        forLocations: true
1163    },
1164    {
1165        id: "city_district_type_full",
1166        fields: ["city_district_type_full"],
1167        forBounds: false,
1168        forLocations: true,
1169        kladrFormat: { digits: 11, zeros: 2 },
1170        fiasType: "city_district_fias_id"
1171    },
1172    {
1173        id: "city_district",
1174        fields: [
1175            "city_district",
1176            "city_district_type",
1177            "city_district_type_full",
1178            "city_district_with_type"
1179        ],
1180        forBounds: true,
1181        forLocations: true,
1182        kladrFormat: { digits: 11, zeros: 2 },
1183        fiasType: "city_district_fias_id"
1184    },
1185    {
1186        id: "settlement_fias_id",
1187        fields: ["settlement_fias_id"],
1188        forBounds: false,
1189        forLocations: true
1190    },
1191    {
1192        id: "settlement_type_full",
1193        fields: ["settlement_type_full"],
1194        forBounds: false,
1195        forLocations: true,
1196        kladrFormat: { digits: 11, zeros: 2 },
1197        fiasType: "settlement_fias_id"
1198    },
1199    {
1200        id: "settlement",
1201        fields: [
1202            "settlement",
1203            "settlement_type",
1204            "settlement_type_full",
1205            "settlement_with_type"
1206        ],
1207        forBounds: true,
1208        forLocations: true,
1209        kladrFormat: { digits: 11, zeros: 2 },
1210        fiasType: "settlement_fias_id"
1211    },
1212    {
1213        id: "street_fias_id",
1214        fields: ["street_fias_id"],
1215        forBounds: false,
1216        forLocations: true
1217    },
1218    {
1219        id: "street_type_full",
1220        fields: ["street_type_full"],
1221        forBounds: false,
1222        forLocations: true,
1223        kladrFormat: { digits: 15, zeros: 2 },
1224        fiasType: "street_fias_id"
1225    },
1226    {
1227        id: "street",
1228        fields: [
1229            "street",
1230            "street_type",
1231            "street_type_full",
1232            "street_with_type"
1233        ],
1234        forBounds: true,
1235        forLocations: true,
1236        kladrFormat: { digits: 15, zeros: 2 },
1237        fiasType: "street_fias_id"
1238    },
1239    {
1240        id: "house",
1241        fields: [
1242            "house",
1243            "house_type",
1244            "house_type_full",
1245            "block",
1246            "block_type"
1247        ],
1248        forBounds: true,
1249        forLocations: true,
1250        kladrFormat: { digits: 19 },
1251        fiasType: "house_fias_id"
1252    },
1253    {
1254        id: "flat",
1255        fields: ["flat", "flat_type", "flat_type_full"],
1256        forBounds: true,
1257        forLocations: false,
1258        kladrFormat: { digits: 19 },
1259        fiasType: "flat_fias_id"
1260    }
1261];
1262
1263var ADDRESS_TYPE = {
1264    urlSuffix: "address",
1265    noSuggestionsHint: "Неизвестный адрес",
1266    matchers: [
1267        matchers.matchByNormalizedQuery(ADDRESS_STOPWORDS),
1268        matchers.matchByWordsAddress(ADDRESS_STOPWORDS)
1269    ],
1270    dataComponents: ADDRESS_COMPONENTS,
1271    dataComponentsById: object_util.indexObjectsById(
1272        ADDRESS_COMPONENTS,
1273        "id",
1274        "index"
1275    ),
1276    unformattableTokens: ADDRESS_STOPWORDS,
1277    enrichmentEnabled: true,
1278    enrichmentMethod: "suggest",
1279    enrichmentParams: {
1280        count: 1,
1281        locations: null,
1282        locations_boost: null,
1283        from_bound: null,
1284        to_bound: null
1285    },
1286    getEnrichmentQuery: function(suggestion) {
1287        return suggestion.unrestricted_value;
1288    },
1289    geoEnabled: true,
1290    isDataComplete: function(suggestion) {
1291        var fields = [this.bounds.to || "flat"],
1292            data = suggestion.data;
1293
1294        return (
1295            !lang_util.isPlainObject(data) ||
1296            object_util.fieldsAreNotEmpty(data, fields)
1297        );
1298    },
1299    composeValue: function(data, options) {
1300        var country = data.country,
1301            region =
1302                data.region_with_type ||
1303                collection_util
1304                    .compact([data.region, data.region_type])
1305                    .join(" ") ||
1306                data.region_type_full,
1307            area =
1308                data.area_with_type ||
1309                collection_util
1310                    .compact([data.area_type, data.area])
1311                    .join(" ") ||
1312                data.area_type_full,
1313            city =
1314                data.city_with_type ||
1315                collection_util
1316                    .compact([data.city_type, data.city])
1317                    .join(" ") ||
1318                data.city_type_full,
1319            settelement =
1320                data.settlement_with_type ||
1321                collection_util
1322                    .compact([data.settlement_type, data.settlement])
1323                    .join(" ") ||
1324                data.settlement_type_full,
1325            cityDistrict =
1326                data.city_district_with_type ||
1327                collection_util
1328                    .compact([data.city_district_type, data.city_district])
1329                    .join(" ") ||
1330                data.city_district_type_full,
1331            street =
1332                data.street_with_type ||
1333                collection_util
1334                    .compact([data.street_type, data.street])
1335                    .join(" ") ||
1336                data.street_type_full,
1337            house = collection_util
1338                .compact([
1339                    data.stead_type,
1340                    data.stead,
1341                    data.house_type,
1342                    data.house,
1343                    data.block_type,
1344                    data.block
1345                ])
1346                .join(" "),
1347            flat = collection_util
1348                .compact([data.flat_type, data.flat])
1349                .join(" "),
1350            postal_box = data.postal_box && "а/я " + data.postal_box,
1351            result;
1352
1353        // если регион совпадает с городом
1354        // например г Москва, г Москва
1355        // то не показываем регион
1356        if (region === city) {
1357            region = "";
1358        }
1359
1360        // иногда не показываем район
1361        if (!(options && options.saveCityDistrict)) {
1362            if (options && options.excludeCityDistrict) {
1363                // если район явно запрещен
1364                cityDistrict = "";
1365            } else if (cityDistrict && !data.city_district_fias_id) {
1366                // если район взят из ОКАТО (у него пустой city_district_fias_id)
1367                cityDistrict = "";
1368            }
1369        }
1370
1371        result = collection_util
1372            .compact([
1373                country,
1374                region,
1375                area,
1376                city,
1377                cityDistrict,
1378                settelement,
1379                street,
1380                house,
1381                flat,
1382                postal_box
1383            ])
1384            .join(", ");
1385
1386        return result;
1387    },
1388    formatResult: (function() {
1389        var componentsUnderCityDistrict = [],
1390            _underCityDistrict = false;
1391
1392        ADDRESS_COMPONENTS.forEach(function(component) {
1393            if (_underCityDistrict)
1394                componentsUnderCityDistrict.push(component.id);
1395            if (component.id === "city_district") _underCityDistrict = true;
1396        });
1397
1398        return function(value, currentValue, suggestion, options) {
1399            var that = this,
1400                district =
1401                    suggestion.data && suggestion.data.city_district_with_type,
1402                unformattableTokens = options && options.unformattableTokens,
1403                historyValues =
1404                    suggestion.data && suggestion.data.history_values,
1405                tokens,
1406                unusedTokens,
1407                formattedHistoryValues;
1408
1409            // добавляем исторические значения
1410            if (historyValues && historyValues.length > 0) {
1411                tokens = text_util.tokenize(currentValue, unformattableTokens);
1412                unusedTokens = this.type.findUnusedTokens(tokens, value);
1413                formattedHistoryValues = this.type.getFormattedHistoryValues(
1414                    unusedTokens,
1415                    historyValues
1416                );
1417                if (formattedHistoryValues) {
1418                    value += formattedHistoryValues;
1419                }
1420            }
1421
1422            value = that.highlightMatches(
1423                value,
1424                currentValue,
1425                suggestion,
1426                options
1427            );
1428            value = that.wrapFormattedValue(value, suggestion);
1429
1430            if (
1431                district &&
1432                (!that.bounds.own.length ||
1433                    that.bounds.own.indexOf("street") >= 0) &&
1434                !lang_util.isEmptyObject(
1435                    that.copyDataComponents(
1436                        suggestion.data,
1437                        componentsUnderCityDistrict
1438                    )
1439                )
1440            ) {
1441                value +=
1442                    '<div class="' +
1443                    that.classes.subtext +
1444                    '">' +
1445                    that.highlightMatches(district, currentValue, suggestion) +
1446                    "</div>";
1447            }
1448
1449            return value;
1450        };
1451    })(),
1452
1453    /**
1454     * Возвращает список слов в запросе,
1455     * которые не встречаются в подсказке
1456     */
1457    findUnusedTokens: function(tokens, value) {
1458        var tokenIndex,
1459            token,
1460            unused = [];
1461
1462        unused = tokens.filter(function(token) {
1463            return value.indexOf(token) === -1;
1464        });
1465
1466        return unused;
1467    },
1468
1469    /**
1470     * Возвращает исторические названия для слов запроса,
1471     * для которых не найдено совпадения в основном значении подсказки
1472     */
1473    getFormattedHistoryValues: function(unusedTokens, historyValues) {
1474        var tokenIndex,
1475            token,
1476            historyValueIndex,
1477            historyValue,
1478            values = [],
1479            formatted = "";
1480
1481        historyValues.forEach(function(historyValue) {
1482            collection_util.each(unusedTokens, function(token) {
1483                if (historyValue.toLowerCase().indexOf(token) >= 0) {
1484                    values.push(historyValue);
1485                    return false;
1486                }
1487            });
1488        });
1489
1490        if (values.length > 0) {
1491            formatted = " (бывш. " + values.join(", ") + ")";
1492        }
1493
1494        return formatted;
1495    },
1496
1497    /**
1498     * @param instance
1499     * @param options
1500     * @param options.suggestion
1501     * @param options.hasSameValues
1502     * @param options.hasBeenEnreached
1503     */
1504    getSuggestionValue: function(instance, options) {
1505        var formattedValue = null;
1506
1507        if (options.hasSameValues) {
1508            if (instance.options.restrict_value) {
1509                // Can not use unrestricted address,
1510                // because some components (from constraints) must be omitted
1511                formattedValue = this.getValueWithinConstraints(
1512                    instance,
1513                    options.suggestion
1514                );
1515            } else if (instance.bounds.own.length) {
1516                // Can not use unrestricted address,
1517                // because only components from bounds must be included
1518                formattedValue = this.getValueWithinBounds(
1519                    instance,
1520                    options.suggestion
1521                );
1522            } else {
1523                // Can use full unrestricted address
1524                formattedValue = options.suggestion.unrestricted_value;
1525            }
1526        } else if (options.hasBeenEnriched) {
1527            if (instance.options.restrict_value) {
1528                formattedValue = this.getValueWithinConstraints(
1529                    instance,
1530                    options.suggestion,
1531                    { excludeCityDistrict: true }
1532                );
1533            }
1534        }
1535
1536        return formattedValue;
1537    },
1538    /*
1539     * Compose suggestion value with respect to constraints
1540     */
1541    getValueWithinConstraints: function(instance, suggestion, options) {
1542        return this.composeValue(
1543            instance.getUnrestrictedData(suggestion.data),
1544            options
1545        );
1546    },
1547    /*
1548     * Compose suggestion value with respect to bounds
1549     */
1550    getValueWithinBounds: function(instance, suggestion, options) {
1551        // для корректного составления адреса нужен city_district_fias_id
1552        var data = instance.copyDataComponents(
1553            suggestion.data,
1554            instance.bounds.own.concat(["city_district_fias_id"])
1555        );
1556
1557        return this.composeValue(data, options);
1558    }
1559};
1560
1561/**
1562 * Компоненты адреса
1563 * @type {*[]}
1564 * id {String} Наименование типа
1565 * fields {Array of Strings}
1566 * forBounds {Boolean} может использоваться в ограничениях
1567 * forLocations {Boolean}
1568 * kladrFormat {Object}
1569 * fiasType {String} Наименование соответствующего ФИАС типа
1570 */
1571var FIAS_COMPONENTS = [
1572    {
1573        id: "kladr_id",
1574        fields: ["kladr_id"],
1575        forBounds: false,
1576        forLocations: true
1577    },
1578    {
1579        id: "region_fias_id",
1580        fields: ["region_fias_id"],
1581        forBounds: false,
1582        forLocations: true
1583    },
1584    {
1585        id: "region_type_full",
1586        fields: ["region_type_full"],
1587        forBounds: false,
1588        forLocations: true,
1589        kladrFormat: { digits: 2, zeros: 11 },
1590        fiasType: "region_fias_id"
1591    },
1592    {
1593        id: "region",
1594        fields: [
1595            "region",
1596            "region_type",
1597            "region_type_full",
1598            "region_with_type"
1599        ],
1600        forBounds: true,
1601        forLocations: true,
1602        kladrFormat: { digits: 2, zeros: 11 },
1603        fiasType: "region_fias_id"
1604    },
1605    {
1606        id: "area_fias_id",
1607        fields: ["area_fias_id"],
1608        forBounds: false,
1609        forLocations: true
1610    },
1611    {
1612        id: "area_type_full",
1613        fields: ["area_type_full"],
1614        forBounds: false,
1615        forLocations: true,
1616        kladrFormat: { digits: 5, zeros: 8 },
1617        fiasType: "area_fias_id"
1618    },
1619    {
1620        id: "area",
1621        fields: ["area", "area_type", "area_type_full", "area_with_type"],
1622        forBounds: true,
1623        forLocations: true,
1624        kladrFormat: { digits: 5, zeros: 8 },
1625        fiasType: "area_fias_id"
1626    },
1627    {
1628        id: "city_fias_id",
1629        fields: ["city_fias_id"],
1630        forBounds: false,
1631        forLocations: true
1632    },
1633    {
1634        id: "city_type_full",
1635        fields: ["city_type_full"],
1636        forBounds: false,
1637        forLocations: true,
1638        kladrFormat: { digits: 8, zeros: 5 },
1639        fiasType: "city_fias_id"
1640    },
1641    {
1642        id: "city",
1643        fields: ["city", "city_type", "city_type_full", "city_with_type"],
1644        forBounds: true,
1645        forLocations: true,
1646        kladrFormat: { digits: 8, zeros: 5 },
1647        fiasType: "city_fias_id"
1648    },
1649    {
1650        id: "city_district_fias_id",
1651        fields: ["city_district_fias_id"],
1652        forBounds: false,
1653        forLocations: true
1654    },
1655    {
1656        id: "city_district_type_full",
1657        fields: ["city_district_type_full"],
1658        forBounds: false,
1659        forLocations: true,
1660        kladrFormat: { digits: 11, zeros: 2 },
1661        fiasType: "city_district_fias_id"
1662    },
1663    {
1664        id: "city_district",
1665        fields: [
1666            "city_district",
1667            "city_district_type",
1668            "city_district_type_full",
1669            "city_district_with_type"
1670        ],
1671        forBounds: true,
1672        forLocations: true,
1673        kladrFormat: { digits: 11, zeros: 2 },
1674        fiasType: "city_district_fias_id"
1675    },
1676    {
1677        id: "settlement_fias_id",
1678        fields: ["settlement_fias_id"],
1679        forBounds: false,
1680        forLocations: true
1681    },
1682    {
1683        id: "settlement_type_full",
1684        fields: ["settlement_type_full"],
1685        forBounds: false,
1686        forLocations: true,
1687        kladrFormat: { digits: 11, zeros: 2 },
1688        fiasType: "settlement_fias_id"
1689    },
1690    {
1691        id: "settlement",
1692        fields: [
1693            "settlement",
1694            "settlement_type",
1695            "settlement_type_full",
1696            "settlement_with_type"
1697        ],
1698        forBounds: true,
1699        forLocations: true,
1700        kladrFormat: { digits: 11, zeros: 2 },
1701        fiasType: "settlement_fias_id"
1702    },
1703    {
1704        id: "planning_structure_fias_id",
1705        fields: ["planning_structure_fias_id"],
1706        forBounds: false,
1707        forLocations: true
1708    },
1709    {
1710        id: "planning_structure_type_full",
1711        fields: ["planning_structure_type_full"],
1712        forBounds: false,
1713        forLocations: true,
1714        kladrFormat: { digits: 15, zeros: 2 },
1715        fiasType: "planning_structure_fias_id"
1716    },
1717    {
1718        id: "planning_structure",
1719        fields: [
1720            "planning_structure",
1721            "planning_structure_type",
1722            "planning_structure_type_full",
1723            "planning_structure_with_type"
1724        ],
1725        forBounds: true,
1726        forLocations: true,
1727        kladrFormat: { digits: 15, zeros: 2 },
1728        fiasType: "planning_structure_fias_id"
1729    },
1730    {
1731        id: "street_fias_id",
1732        fields: ["street_fias_id"],
1733        forBounds: false,
1734        forLocations: true
1735    },
1736    {
1737        id: "street_type_full",
1738        fields: ["street_type_full"],
1739        forBounds: false,
1740        forLocations: true,
1741        kladrFormat: { digits: 15, zeros: 2 },
1742        fiasType: "street_fias_id"
1743    },
1744    {
1745        id: "street",
1746        fields: [
1747            "street",
1748            "street_type",
1749            "street_type_full",
1750            "street_with_type"
1751        ],
1752        forBounds: true,
1753        forLocations: true,
1754        kladrFormat: { digits: 15, zeros: 2 },
1755        fiasType: "street_fias_id"
1756    },
1757    {
1758        id: "house",
1759        fields: ["house", "house_type", "block", "building_type", "building"],
1760        forBounds: true,
1761        forLocations: false,
1762        kladrFormat: { digits: 19 }
1763    }
1764];
1765
1766var FIAS_TYPE = {
1767    urlSuffix: "fias",
1768    noSuggestionsHint: "Неизвестный адрес",
1769    matchers: [
1770        matchers.matchByNormalizedQuery(ADDRESS_STOPWORDS),
1771        matchers.matchByWordsAddress(ADDRESS_STOPWORDS)
1772    ],
1773    dataComponents: FIAS_COMPONENTS,
1774    dataComponentsById: object_util.indexObjectsById(
1775        FIAS_COMPONENTS,
1776        "id",
1777        "index"
1778    ),
1779    unformattableTokens: ADDRESS_STOPWORDS,
1780    isDataComplete: function(suggestion) {
1781        var fields = [this.bounds.to || "house"],
1782            data = suggestion.data;
1783
1784        return (
1785            !lang_util.isPlainObject(data) ||
1786            object_util.fieldsAreNotEmpty(data, fields)
1787        );
1788    },
1789    composeValue: function(data, options) {
1790        var country = data.country,
1791            region =
1792                data.region_with_type ||
1793                collection_util
1794                    .compact([data.region, data.region_type])
1795                    .join(" ") ||
1796                data.region_type_full,
1797            area =
1798                data.area_with_type ||
1799                collection_util
1800                    .compact([data.area_type, data.area])
1801                    .join(" ") ||
1802                data.area_type_full,
1803            city =
1804                data.city_with_type ||
1805                collection_util
1806                    .compact([data.city_type, data.city])
1807                    .join(" ") ||
1808                data.city_type_full,
1809            settelement =
1810                data.settlement_with_type ||
1811                collection_util
1812                    .compact([data.settlement_type, data.settlement])
1813                    .join(" ") ||
1814                data.settlement_type_full,
1815            cityDistrict =
1816                data.city_district_with_type ||
1817                collection_util
1818                    .compact([data.city_district_type, data.city_district])
1819                    .join(" ") ||
1820                data.city_district_type_full,
1821            planning_structure =
1822                data.planning_structure_with_type ||
1823                collection_util
1824                    .compact([
1825                        data.planning_structure_type,
1826                        data.planning_structure
1827                    ])
1828                    .join(" ") ||
1829                data.planning_structure_type_full,
1830            street =
1831                data.street_with_type ||
1832                collection_util
1833                    .compact([data.street_type, data.street])
1834                    .join(" ") ||
1835                data.street_type_full,
1836            house = collection_util
1837                .compact([
1838                    data.house_type,
1839                    data.house,
1840                    data.block_type,
1841                    data.block
1842                ])
1843                .join(" "),
1844            flat = collection_util
1845                .compact([data.flat_type, data.flat])
1846                .join(" "),
1847            postal_box = data.postal_box && "а/я " + data.postal_box,
1848            result;
1849
1850        // если регион совпадает с городом
1851        // например г Москва, г Москва
1852        // то не показываем регион
1853        if (region === city) {
1854            region = "";
1855        }
1856
1857        // иногда не показываем район
1858        if (!(options && options.saveCityDistrict)) {
1859            if (options && options.excludeCityDistrict) {
1860                // если район явно запрещен
1861                cityDistrict = "";
1862            } else if (cityDistrict && !data.city_district_fias_id) {
1863                // если район взят из ОКАТО (у него пустой city_district_fias_id)
1864                cityDistrict = "";
1865            }
1866        }
1867
1868        result = collection_util
1869            .compact([
1870                country,
1871                region,
1872                area,
1873                city,
1874                cityDistrict,
1875                settelement,
1876                planning_structure,
1877                street,
1878                house,
1879                flat,
1880                postal_box
1881            ])
1882            .join(", ");
1883
1884        return result;
1885    },
1886    formatResult: (function() {
1887        return function(value, currentValue, suggestion, options) {
1888            var that = this;
1889
1890            value = that.highlightMatches(
1891                value,
1892                currentValue,
1893                suggestion,
1894                options
1895            );
1896            value = that.wrapFormattedValue(value, suggestion);
1897            return value;
1898        };
1899    })(),
1900
1901    getSuggestionValue: ADDRESS_TYPE.getSuggestionValue,
1902    getValueWithinConstraints: ADDRESS_TYPE.getValueWithinConstraints,
1903    getValueWithinBounds: ADDRESS_TYPE.getValueWithinBounds
1904};
1905
1906function valueStartsWith(suggestion, field) {
1907    var fieldValue = suggestion.data && suggestion.data[field];
1908
1909    return (
1910        fieldValue &&
1911        new RegExp(
1912            "^" +
1913                text_util.escapeRegExChars(fieldValue) +
1914                "([" +
1915                WORD_DELIMITERS +
1916                "]|$)",
1917            "i"
1918        ).test(suggestion.value)
1919    );
1920}
1921
1922var NAME_TYPE = {
1923    urlSuffix: "fio",
1924    noSuggestionsHint: false,
1925    matchers: [matchers.matchByNormalizedQuery(), matchers.matchByWords()],
1926    // names for labels, describing which fields are displayed
1927    fieldNames: {
1928        surname: "фамилия",
1929        name: "имя",
1930        patronymic: "отчество"
1931    },
1932    isDataComplete: function(suggestion) {
1933        var that = this,
1934            params = that.options.params,
1935            data = suggestion.data,
1936            fields;
1937
1938        if (lang_util.isFunction(params)) {
1939            params = params.call(that.element, suggestion.value);
1940        }
1941        if (params && params.parts) {
1942            fields = params.parts.map(function(part) {
1943                return part.toLowerCase();
1944            });
1945        } else {
1946            // when NAME is first, patronymic is mot mandatory
1947            fields = ["surname", "name"];
1948            // when SURNAME is first, it is
1949            if (valueStartsWith(suggestion, "surname")) {
1950                fields.push("patronymic");
1951            }
1952        }
1953        return object_util.fieldsAreNotEmpty(data, fields);
1954    },
1955    composeValue: function(data) {
1956        return collection_util
1957            .compact([data.surname, data.name, data.patronymic])
1958            .join(" ");
1959    }
1960};
1961
1962var innPartsLengths = {
1963    LEGAL: [2, 2, 5, 1],
1964    INDIVIDUAL: [2, 2, 6, 2]
1965};
1966
1967function chooseFormattedField(formattedMain, formattedAlt) {
1968    var rHasMatch = /<strong>/;
1969    return rHasMatch.test(formattedAlt) && !rHasMatch.test(formattedMain)
1970        ? formattedAlt
1971        : formattedMain;
1972}
1973
1974function formattedField(main, alt, currentValue, suggestion, options) {
1975    var that = this,
1976        formattedMain = that.highlightMatches(
1977            main,
1978            currentValue,
1979            suggestion,
1980            options
1981        ),
1982        formattedAlt = that.highlightMatches(
1983            alt,
1984            currentValue,
1985            suggestion,
1986            options
1987        );
1988
1989    return chooseFormattedField(formattedMain, formattedAlt);
1990}
1991
1992var PARTY_TYPE = {
1993    urlSuffix: "party",
1994    noSuggestionsHint: "Неизвестная организация",
1995    matchers: [
1996        matchers.matchByFields(
1997            // These fields of suggestion's `data` used by by-words matcher
1998            {
1999                value: null,
2000                "data.address.value": ADDRESS_STOPWORDS,
2001                "data.inn": null,
2002                "data.ogrn": null
2003            }
2004        )
2005    ],
2006    dataComponents: ADDRESS_COMPONENTS,
2007    enrichmentEnabled: true,
2008    enrichmentMethod: "findById",
2009    enrichmentParams: {
2010        count: 1,
2011        locations_boost: null
2012    },
2013    getEnrichmentQuery: function(suggestion) {
2014        return suggestion.data.hid;
2015    },
2016    geoEnabled: true,
2017    formatResult: function(value, currentValue, suggestion, options) {
2018        var that = this,
2019            formattedInn = that.type.formatResultInn.call(
2020                that,
2021                suggestion,
2022                currentValue
2023            ),
2024            formatterOGRN = that.highlightMatches(
2025                object_util.getDeepValue(suggestion.data, "ogrn"),
2026                currentValue,
2027                suggestion
2028            ),
2029            formattedInnOGRN = chooseFormattedField(
2030                formattedInn,
2031                formatterOGRN
2032            ),
2033            formattedFIO = that.highlightMatches(
2034                object_util.getDeepValue(suggestion.data, "management.name"),
2035                currentValue,
2036                suggestion
2037            ),
2038            address =
2039                object_util.getDeepValue(suggestion.data, "address.value") ||
2040                "";
2041
2042        if (that.isMobile) {
2043            (options || (options = {})).maxLength = 50;
2044        }
2045
2046        value = formattedField.call(
2047            that,
2048            value,
2049            object_util.getDeepValue(suggestion.data, "name.latin"),
2050            currentValue,
2051            suggestion,
2052            options
2053        );
2054        value = that.wrapFormattedValue(value, suggestion);
2055
2056        if (address) {
2057            address = address.replace(/^(\d{6}|Россия),\s+/i, "");
2058            if (that.isMobile) {
2059                // keep only two first words
2060                address = address.replace(
2061                    new RegExp(
2062                        "^([^" +
2063                            WORD_DELIMITERS +
2064                            "]+[" +
2065                            WORD_DELIMITERS +
2066                            "]+[^" +
2067                            WORD_DELIMITERS +
2068                            "]+).*"
2069                    ),
2070                    "$1"
2071                );
2072            } else {
2073                address = that.highlightMatches(
2074                    address,
2075                    currentValue,
2076                    suggestion,
2077                    {
2078                        unformattableTokens: ADDRESS_STOPWORDS
2079                    }
2080                );
2081            }
2082        }
2083
2084        if (formattedInnOGRN || address || formattedFIO) {
2085            value +=
2086                '<div class="' +
2087                that.classes.subtext +
2088                '">' +
2089                '<span class="' +
2090                that.classes.subtext_inline +
2091                '">' +
2092                (formattedInnOGRN || "") +
2093                "</span>" +
2094                (chooseFormattedField(address, formattedFIO) || "") +
2095                "</div>";
2096        }
2097        return value;
2098    },
2099    formatResultInn: function(suggestion, currentValue) {
2100        var that = this,
2101            inn = suggestion.data && suggestion.data.inn,
2102            innPartsLength =
2103                innPartsLengths[suggestion.data && suggestion.data.type],
2104            innParts,
2105            formattedInn,
2106            rDigit = /\d/;
2107
2108        if (inn) {
2109            formattedInn = that.highlightMatches(inn, currentValue, suggestion);
2110            if (innPartsLength) {
2111                formattedInn = formattedInn.split("");
2112                innParts = innPartsLength.map(function(partLength) {
2113                    var formattedPart = "",
2114                        ch;
2115
2116                    while (partLength && (ch = formattedInn.shift())) {
2117                        formattedPart += ch;
2118                        if (rDigit.test(ch)) partLength--;
2119                    }
2120
2121                    return formattedPart;
2122                });
2123                formattedInn =
2124                    innParts.join(
2125                        '<span class="' +
2126                            that.classes.subtext_delimiter +
2127                            '"></span>'
2128                    ) + formattedInn.join("");
2129            }
2130
2131            return formattedInn;
2132        }
2133    }
2134};
2135
2136var EMAIL_TYPE = {
2137    urlSuffix: "email",
2138    noSuggestionsHint: false,
2139    matchers: [matchers.matchByNormalizedQuery()],
2140    isQueryRequestable: function(query) {
2141        return this.options.suggest_local || query.indexOf("@") >= 0;
2142    }
2143};
2144
2145var BANK_TYPE = {
2146    urlSuffix: "bank",
2147    noSuggestionsHint: "Неизвестный банк",
2148    matchers: [
2149        matchers.matchByFields(
2150            // These fields of suggestion's `data` used by by-words matcher
2151            {
2152                value: null,
2153                "data.bic": null,
2154                "data.swift": null,
2155            }
2156        ),
2157    ],
2158    dataComponents: ADDRESS_COMPONENTS,
2159    enrichmentEnabled: true,
2160    enrichmentMethod: "findById",
2161    enrichmentParams: {
2162        count: 1,
2163    },
2164    getEnrichmentQuery: function(suggestion) {
2165        return suggestion.data.bic;
2166    },
2167    geoEnabled: true,
2168    formatResult: function(value, currentValue, suggestion, options) {
2169        var that = this,
2170            formattedBIC = that.highlightMatches(
2171                object_util.getDeepValue(suggestion.data, "bic"),
2172                currentValue,
2173                suggestion
2174            ),
2175            address =
2176                object_util.getDeepValue(suggestion.data, "address.value") ||
2177                "";
2178
2179        value = that.highlightMatches(value, currentValue, suggestion, options);
2180        value = that.wrapFormattedValue(value, suggestion);
2181
2182        if (address) {
2183            address = address.replace(/^\d{6}( РОССИЯ)?, /i, "");
2184            if (that.isMobile) {
2185                // keep only two first words
2186                address = address.replace(
2187                    new RegExp(
2188                        "^([^" +
2189                            WORD_DELIMITERS +
2190                            "]+[" +
2191                            WORD_DELIMITERS +
2192                            "]+[^" +
2193                            WORD_DELIMITERS +
2194                            "]+).*"
2195                    ),
2196                    "$1"
2197                );
2198            } else {
2199                address = that.highlightMatches(
2200                    address,
2201                    currentValue,
2202                    suggestion,
2203                    {
2204                        unformattableTokens: ADDRESS_STOPWORDS,
2205                    }
2206                );
2207            }
2208        }
2209
2210        if (formattedBIC || address) {
2211            value +=
2212                '<div class="' +
2213                that.classes.subtext +
2214                '">' +
2215                '<span class="' +
2216                that.classes.subtext_inline +
2217                '">' +
2218                formattedBIC +
2219                "</span>" +
2220                address +
2221                "</div>";
2222        }
2223        return value;
2224    },
2225    formatSelected: function(suggestion) {
2226        return (
2227            object_util.getDeepValue(suggestion, "data.name.payment") || null
2228        );
2229    },
2230};
2231
2232function Outward(name) {
2233    this.urlSuffix = name.toLowerCase();
2234    this.noSuggestionsHint = "Неизвестное значение";
2235    this.matchers = [
2236        matchers.matchByNormalizedQuery(),
2237        matchers.matchByWords()
2238    ];
2239}
2240
2241/**
2242 * Type is a bundle of properties:
2243 * - urlSuffix Mandatory. String
2244 * - matchers Mandatory. Array of functions (with optional data bound as a context) that find appropriate suggestion to select
2245 * - `fieldNames` Map fields of suggestion.data to their displayable names
2246 * - `unformattableTokens` Array of strings which should not be highlighted
2247 * - `dataComponents` Array of 'bound's can be set as `bounds` option. Order is important.
2248 *
2249 * flags:
2250 * - `alwaysContinueSelecting` Forbids to hide dropdown after selecting
2251 * - `geoEnabled` Makes to detect client's location for passing it to all requests
2252 * - `enrichmentEnabled` Makes to send additional request when a suggestion is selected
2253 *
2254 * and methods:
2255 * - `isDataComplete` Checks if suggestion.data can be operated as full data of it's type
2256 * - `composeValue` returns string value based on suggestion.data
2257 * - `formatResult` returns html of a suggestion. Overrides default method
2258 * - `formatResultInn` returns html of suggestion.data.inn
2259 * - `isQueryRequestable` checks if query is appropriated for requesting server
2260 * - `formatSelected` returns string to be inserted in textbox
2261 */
2262
2263var types = {
2264    NAME: NAME_TYPE,
2265    ADDRESS: ADDRESS_TYPE,
2266    FIAS: FIAS_TYPE,
2267    PARTY: PARTY_TYPE,
2268    EMAIL: EMAIL_TYPE,
2269    BANK: BANK_TYPE
2270};
2271
2272types.get = function(type) {
2273    if (types.hasOwnProperty(type)) {
2274        return types[type];
2275    } else {
2276        return new Outward(type);
2277    }
2278};
2279
2280jqapi.extend(DEFAULT_OPTIONS, {
2281    suggest_local: true
2282});
2283
2284var notificator = {
2285    chains: {},
2286
2287    on: function(name, method) {
2288        this.get(name).push(method);
2289        return this;
2290    },
2291
2292    get: function(name) {
2293        var chains = this.chains;
2294        return chains[name] || (chains[name] = []);
2295    }
2296};
2297
2298var serviceMethods = {
2299    suggest: {
2300        defaultParams: {
2301            type: utils.getDefaultType(),
2302            dataType: "json",
2303            contentType: utils.getDefaultContentType()
2304        },
2305        addTypeInUrl: true
2306    },
2307    "iplocate/address": {
2308        defaultParams: {
2309            type: "GET",
2310            dataType: "json"
2311        },
2312        addTypeInUrl: false
2313    },
2314    status: {
2315        defaultParams: {
2316            type: "GET",
2317            dataType: "json"
2318        },
2319        addTypeInUrl: true
2320    },
2321    findById: {
2322        defaultParams: {
2323            type: utils.getDefaultType(),
2324            dataType: "json",
2325            contentType: utils.getDefaultContentType()
2326        },
2327        addTypeInUrl: true
2328    }
2329};
2330
2331var requestModes = {
2332    suggest: {
2333        method: "suggest",
2334        userSelect: true,
2335        updateValue: true,
2336        enrichmentEnabled: true
2337    },
2338    findById: {
2339        method: "findById",
2340        userSelect: false,
2341        updateValue: false,
2342        enrichmentEnabled: false
2343    }
2344};
2345
2346function Suggestions(el, options) {
2347    var that = this;
2348
2349    // Shared variables:
2350    that.element = el;
2351    that.el = $(el);
2352    that.suggestions = [];
2353    that.badQueries = [];
2354    that.selectedIndex = -1;
2355    that.currentValue = that.element.value;
2356    that.intervalId = 0;
2357    that.cachedResponse = {};
2358    that.enrichmentCache = {};
2359    that.currentRequest = null;
2360    that.inputPhase = $.Deferred();
2361    that.fetchPhase = $.Deferred();
2362    that.enrichPhase = $.Deferred();
2363    that.onChangeTimeout = null;
2364    that.triggering = {};
2365    that.$wrapper = null;
2366    that.options = $.extend({}, DEFAULT_OPTIONS, options);
2367    that.classes = CLASSES;
2368    that.disabled = false;
2369    that.selection = null;
2370    that.$viewport = $(window);
2371    that.$body = $(document.body);
2372    that.type = null;
2373    that.status = {};
2374
2375    that.setupElement();
2376
2377    that.initializer = $.Deferred();
2378
2379    if (that.el.is(":visible")) {
2380        that.initializer.resolve();
2381    } else {
2382        that.deferInitialization();
2383    }
2384
2385    that.initializer.done($.proxy(that.initialize, that));
2386}
2387
2388Suggestions.prototype = {
2389    // Creation and destruction
2390
2391    initialize: function() {
2392        var that = this;
2393        that.uniqueId = utils.uniqueId("i");
2394        that.createWrapper();
2395        that.notify("initialize");
2396        that.bindWindowEvents();
2397        that.setOptions();
2398        that.inferIsMobile();
2399        that.notify("ready");
2400    },
2401
2402    /**
2403     * Initialize when element is firstly interacted
2404     */
2405    deferInitialization: function() {
2406        var that = this,
2407            events = "mouseover focus keydown",
2408            timer,
2409            callback = function() {
2410                that.initializer.resolve();
2411                that.enable();
2412            };
2413
2414        that.initializer.always(function() {
2415            that.el.off(events, callback);
2416            clearInterval(timer);
2417        });
2418
2419        that.disabled = true;
2420        that.el.on(events, callback);
2421        timer = setInterval(function() {
2422            if (that.el.is(":visible")) {
2423                callback();
2424            }
2425        }, that.options.initializeInterval);
2426    },
2427
2428    isInitialized: function() {
2429        return this.initializer.state() === "resolved";
2430    },
2431
2432    dispose: function() {
2433        var that = this;
2434        that.initializer.reject();
2435        that.notify("dispose");
2436        that.el.removeData(DATA_ATTR_KEY).removeClass("suggestions-input");
2437        that.unbindWindowEvents();
2438        that.removeWrapper();
2439        that.el.trigger("suggestions-dispose");
2440    },
2441
2442    notify: function(chainName) {
2443        var that = this,
2444            args = utils.slice(arguments, 1);
2445
2446        return $.map(notificator.get(chainName), function(method) {
2447            return method.apply(that, args);
2448        });
2449    },
2450
2451    createWrapper: function() {
2452        var that = this;
2453
2454        that.$wrapper = $('<div class="suggestions-wrapper"/>');
2455        that.el.after(that.$wrapper);
2456
2457        that.$wrapper.on(
2458            "mousedown" + EVENT_NS,
2459            $.proxy(that.onMousedown, that)
2460        );
2461    },
2462
2463    removeWrapper: function() {
2464        var that = this;
2465
2466        if (that.$wrapper) {
2467            that.$wrapper.remove();
2468        }
2469        $(that.options.$helpers).off(EVENT_NS);
2470    },
2471
2472    /** This whole handler is needed to prevent blur event on textbox
2473     * when suggestion is clicked (blur leads to suggestions hide, so we need to prevent it).
2474     * See https://github.com/jquery/jquery-ui/blob/master/ui/autocomplete.js for details
2475     */
2476    onMousedown: function(e) {
2477        var that = this;
2478
2479        // prevent moving focus out of the text field
2480        e.preventDefault();
2481
2482        // IE doesn't prevent moving focus even with e.preventDefault()
2483        // so we set a flag to know when we should ignore the blur event
2484        that.cancelBlur = true;
2485        utils.delay(function() {
2486            delete that.cancelBlur;
2487        });
2488
2489        // clicking on the scrollbar causes focus to shift to the body
2490        // but we can't detect a mouseup or a click immediately afterward
2491        // so we have to track the next mousedown and close the menu if
2492        // the user clicks somewhere outside of the autocomplete
2493        if ($(e.target).closest(".ui-menu-item").length == 0) {
2494            utils.delay(function() {
2495                $(document).one("mousedown", function(e) {
2496                    var $elements = that.el
2497                        .add(that.$wrapper)
2498                        .add(that.options.$helpers);
2499
2500                    if (that.options.floating) {
2501                        $elements = $elements.add(that.$container);
2502                    }
2503
2504                    $elements = $elements.filter(function() {
2505                        return this === e.target || $.contains(this, e.target);
2506                    });
2507
2508                    if (!$elements.length) {
2509                        that.hide();
2510                    }
2511                });
2512            });
2513        }
2514    },
2515
2516    bindWindowEvents: function() {
2517        var handler = $.proxy(this.inferIsMobile, this);
2518        this.$viewport.on("resize" + EVENT_NS + this.uniqueId, handler);
2519    },
2520
2521    unbindWindowEvents: function() {
2522        this.$viewport.off("resize" + EVENT_NS + this.uniqueId);
2523    },
2524
2525    scrollToTop: function() {
2526        var that = this,
2527            scrollTarget = that.options.scrollOnFocus;
2528
2529        if (scrollTarget === true) {
2530            scrollTarget = that.el;
2531        }
2532        if (scrollTarget instanceof $ && scrollTarget.length > 0) {
2533            $("body,html").animate(
2534                {
2535                    scrollTop: scrollTarget.offset().top
2536                },
2537                "fast"
2538            );
2539        }
2540    },
2541
2542    // Configuration methods
2543
2544    setOptions: function(suppliedOptions) {
2545        var that = this;
2546
2547        $.extend(that.options, suppliedOptions);
2548
2549        that["type"] = types.get(that.options["type"]);
2550
2551        // Check mandatory options
2552        $.each(
2553            {
2554                requestMode: requestModes
2555            },
2556            function(option, available) {
2557                that[option] = available[that.options[option]];
2558                if (!that[option]) {
2559                    that.disable();
2560                    throw "`" +
2561                        option +
2562                        "` option is incorrect! Must be one of: " +
2563                        $.map(available, function(value, name) {
2564                            return '"' + name + '"';
2565                        }).join(", ");
2566                }
2567            }
2568        );
2569
2570        $(that.options.$helpers)
2571            .off(EVENT_NS)
2572            .on("mousedown" + EVENT_NS, $.proxy(that.onMousedown, that));
2573
2574        if (that.isInitialized()) {
2575            that.notify("setOptions");
2576        }
2577    },
2578
2579    // Common public methods
2580
2581    inferIsMobile: function() {
2582        this.isMobile = this.$viewport.width() <= this.options.mobileWidth;
2583    },
2584
2585    clearCache: function() {
2586        this.cachedResponse = {};
2587        this.enrichmentCache = {};
2588        this.badQueries = [];
2589    },
2590
2591    clear: function() {
2592        var that = this,
2593            currentSelection = that.selection;
2594
2595        if (that.isInitialized()) {
2596            that.clearCache();
2597            that.currentValue = "";
2598            that.selection = null;
2599            that.hide();
2600            that.suggestions = [];
2601            that.el.val("");
2602            that.el.trigger("suggestions-clear");
2603            that.notify("clear");
2604            that.trigger("InvalidateSelection", currentSelection);
2605        }
2606    },
2607
2608    disable: function() {
2609        var that = this;
2610
2611        that.disabled = true;
2612        that.abortRequest();
2613        if (that.visible) {
2614            that.hide();
2615        }
2616    },
2617
2618    enable: function() {
2619        this.disabled = false;
2620    },
2621
2622    isUnavailable: function() {
2623        return this.disabled;
2624    },
2625
2626    update: function() {
2627        var that = this,
2628            query = that.el.val();
2629
2630        if (that.isInitialized()) {
2631            that.currentValue = query;
2632            if (that.isQueryRequestable(query)) {
2633                that.updateSuggestions(query);
2634            } else {
2635                that.hide();
2636            }
2637        }
2638    },
2639
2640    setSuggestion: function(suggestion) {
2641        var that = this,
2642            data,
2643            value;
2644
2645        if ($.isPlainObject(suggestion) && $.isPlainObject(suggestion.data)) {
2646            suggestion = $.extend(true, {}, suggestion);
2647
2648            if (
2649                that.isUnavailable() &&
2650                that.initializer &&
2651                that.initializer.state() === "pending"
2652            ) {
2653                that.initializer.resolve();
2654                that.enable();
2655            }
2656
2657            if (that.bounds.own.length) {
2658                that.checkValueBounds(suggestion);
2659                data = that.copyDataComponents(
2660                    suggestion.data,
2661                    that.bounds.all
2662                );
2663                if (suggestion.data.kladr_id) {
2664                    data.kladr_id = that.getBoundedKladrId(
2665                        suggestion.data.kladr_id,
2666                        that.bounds.all
2667                    );
2668                }
2669                suggestion.data = data;
2670            }
2671
2672            that.selection = suggestion;
2673
2674            // `that.suggestions` required by `that.getSuggestionValue` and must be set before
2675            that.suggestions = [suggestion];
2676            value = that.getSuggestionValue(suggestion) || "";
2677            that.currentValue = value;
2678            that.el.val(value);
2679            that.abortRequest();
2680            that.el.trigger("suggestions-set");
2681        }
2682    },
2683
2684    /**
2685     * Fetch full object for current INPUT's value
2686     * if no suitable object found, clean input element
2687     */
2688    fixData: function() {
2689        var that = this,
2690            fullQuery = that.extendedCurrentValue(),
2691            currentValue = that.el.val(),
2692            resolver = $.Deferred();
2693
2694        resolver
2695            .done(function(suggestion) {
2696                that.selectSuggestion(suggestion, 0, currentValue, {
2697                    hasBeenEnriched: true
2698                });
2699                that.el.trigger("suggestions-fixdata", suggestion);
2700            })
2701            .fail(function() {
2702                that.selection = null;
2703                that.el.trigger("suggestions-fixdata");
2704            });
2705
2706        if (that.isQueryRequestable(fullQuery)) {
2707            that.currentValue = fullQuery;
2708            that.getSuggestions(fullQuery, {
2709                count: 1,
2710                from_bound: null,
2711                to_bound: null
2712            })
2713                .done(function(suggestions) {
2714                    // data fetched
2715                    var suggestion = suggestions[0];
2716                    if (suggestion) {
2717                        resolver.resolve(suggestion);
2718                    } else {
2719                        resolver.reject();
2720                    }
2721                })
2722                .fail(function() {
2723                    // no data fetched
2724                    resolver.reject();
2725                });
2726        } else {
2727            resolver.reject();
2728        }
2729    },
2730
2731    // Querying related methods
2732
2733    /**
2734     * Looks up parent instances
2735     * @returns {String} current value prepended by parents' values
2736     */
2737    extendedCurrentValue: function() {
2738        var that = this,
2739            parentInstance = that.getParentInstance(),
2740            parentValue =
2741                parentInstance && parentInstance.extendedCurrentValue(),
2742            currentValue = $.trim(that.el.val());
2743
2744        return utils.compact([parentValue, currentValue]).join(" ");
2745    },
2746
2747    getAjaxParams: function(method, custom) {
2748        var that = this,
2749            token = $.trim(that.options.token),
2750            partner = $.trim(that.options.partner),
2751            serviceUrl = that.options.serviceUrl,
2752            url = that.options.url,
2753            serviceMethod = serviceMethods[method],
2754            params = $.extend(
2755                {
2756                    timeout: that.options.timeout
2757                },
2758                serviceMethod.defaultParams
2759            ),
2760            headers = {};
2761
2762        if (url) {
2763            serviceUrl = url;
2764        } else {
2765            if (!/\/$/.test(serviceUrl)) {
2766                serviceUrl += "/";
2767            }
2768            serviceUrl += method;
2769            if (serviceMethod.addTypeInUrl) {
2770                serviceUrl += "/" + that.type.urlSuffix;
2771            }
2772        }
2773
2774        serviceUrl = utils.fixURLProtocol(serviceUrl);
2775
2776        if ($.support.cors) {
2777            // for XMLHttpRequest put token in header
2778            if (token) {
2779                headers["Authorization"] = "Token " + token;
2780            }
2781            if (partner) {
2782                headers["X-Partner"] = partner;
2783            }
2784            headers["X-Version"] = Suggestions.version;
2785            if (!params.headers) {
2786                params.headers = {};
2787            }
2788            if (!params.xhrFields) {
2789                params.xhrFields = {};
2790            }
2791            $.extend(params.headers, that.options.headers, headers);
2792            // server sets Access-Control-Allow-Origin: *
2793            // which requires no credentials
2794            params.xhrFields.withCredentials = false;
2795        } else {
2796            // for XDomainRequest put token into URL
2797            if (token) {
2798                headers["token"] = token;
2799            }
2800            if (partner) {
2801                headers["partner"] = partner;
2802            }
2803            headers["version"] = Suggestions.version;
2804            serviceUrl = utils.addUrlParams(serviceUrl, headers);
2805        }
2806
2807        params.url = serviceUrl;
2808
2809        return $.extend(params, custom);
2810    },
2811
2812    isQueryRequestable: function(query) {
2813        var that = this,
2814            result;
2815
2816        result = query.length >= that.options.minChars;
2817
2818        if (result && that.type.isQueryRequestable) {
2819            result = that.type.isQueryRequestable.call(that, query);
2820        }
2821
2822        return result;
2823    },
2824
2825    constructRequestParams: function(query, customParams) {
2826        var that = this,
2827            options = that.options,
2828            params = $.isFunction(options.params)
2829                ? options.params.call(that.element, query)
2830                : $.extend({}, options.params);
2831
2832        if (that.type.constructRequestParams) {
2833            $.extend(params, that.type.constructRequestParams.call(that));
2834        }
2835        $.each(that.notify("requestParams"), function(i, hookParams) {
2836            $.extend(params, hookParams);
2837        });
2838        params[options.paramName] = query;
2839        if ($.isNumeric(options.count) && options.count > 0) {
2840            params.count = options.count;
2841        }
2842        if (options.language) {
2843            params.language = options.language;
2844        }
2845
2846        return $.extend(params, customParams);
2847    },
2848
2849    updateSuggestions: function(query) {
2850        var that = this;
2851
2852        that.fetchPhase = that
2853            .getSuggestions(query)
2854            .done(function(suggestions) {
2855                that.assignSuggestions(suggestions, query);
2856            });
2857    },
2858
2859    /**
2860     * Get suggestions from cache or from server
2861     * @param {String} query
2862     * @param {Object} customParams parameters specified here will be passed to request body
2863     * @param {Object} requestOptions
2864     * @param {Boolean} [requestOptions.noCallbacks]  flag, request competence callbacks will not be invoked
2865     * @param {Boolean} [requestOptions.useEnrichmentCache]
2866     * @return {$.Deferred} waiter which is to be resolved with suggestions as argument
2867     */
2868    getSuggestions: function(query, customParams, requestOptions) {
2869        var response,
2870            that = this,
2871            options = that.options,
2872            noCallbacks = requestOptions && requestOptions.noCallbacks,
2873            useEnrichmentCache =
2874                requestOptions && requestOptions.useEnrichmentCache,
2875            method =
2876                (requestOptions && requestOptions.method) ||
2877                that.requestMode.method,
2878            params = that.constructRequestParams(query, customParams),
2879            cacheKey = $.param(params || {}),
2880            resolver = $.Deferred();
2881
2882        response = that.cachedResponse[cacheKey];
2883        if (response && $.isArray(response.suggestions)) {
2884            resolver.resolve(response.suggestions);
2885        } else {
2886            if (that.isBadQuery(query)) {
2887                resolver.reject();
2888            } else {
2889                if (
2890                    !noCallbacks &&
2891                    options.onSearchStart.call(that.element, params) === false
2892                ) {
2893                    resolver.reject();
2894                } else {
2895                    that.doGetSuggestions(params, method)
2896                        .done(function(response) {
2897                            // if response is correct and current value has not been changed
2898                            if (
2899                                that.processResponse(response) &&
2900                                query == that.currentValue
2901                            ) {
2902                                // Cache results if cache is not disabled:
2903                                if (!options.noCache) {
2904                                    if (useEnrichmentCache) {
2905                                        that.enrichmentCache[query] =
2906                                            response.suggestions[0];
2907                                    } else {
2908                                        that.enrichResponse(response, query);
2909                                        that.cachedResponse[
2910                                            cacheKey
2911                                        ] = response;
2912                                        if (
2913                                            options.preventBadQueries &&
2914                                            response.suggestions.length === 0
2915                                        ) {
2916                                            that.badQueries.push(query);
2917                                        }
2918                                    }
2919                                }
2920
2921                                resolver.resolve(response.suggestions);
2922                            } else {
2923                                resolver.reject();
2924                            }
2925                            if (!noCallbacks) {
2926                                options.onSearchComplete.call(
2927                                    that.element,
2928                                    query,
2929                                    response.suggestions
2930                                );
2931                            }
2932                        })
2933                        .fail(function(jqXHR, textStatus, errorThrown) {
2934                            resolver.reject();
2935                            if (!noCallbacks && textStatus !== "abort") {
2936                                options.onSearchError.call(
2937                                    that.element,
2938                                    query,
2939                                    jqXHR,
2940                                    textStatus,
2941                                    errorThrown
2942                                );
2943                            }
2944                        });
2945                }
2946            }
2947        }
2948        return resolver;
2949    },
2950
2951    /**
2952     * Sends an AJAX request to server suggest method.
2953     * @param {Object} params request params
2954     * @returns {$.Deferred} response promise
2955     */
2956    doGetSuggestions: function(params, method) {
2957        var that = this,
2958            request = $.ajax(
2959                that.getAjaxParams(method, { data: utils.serialize(params) })
2960            );
2961
2962        that.abortRequest();
2963        that.currentRequest = request;
2964        that.notify("request");
2965
2966        request.always(function() {
2967            that.currentRequest = null;
2968            that.notify("request");
2969        });
2970
2971        return request;
2972    },
2973
2974    isBadQuery: function(q) {
2975        if (!this.options.preventBadQueries) {
2976            return false;
2977        }
2978
2979        var result = false;
2980        $.each(this.badQueries, function(i, query) {
2981            return !(result = q.indexOf(query) === 0);
2982        });
2983        return result;
2984    },
2985
2986    abortRequest: function() {
2987        var that = this;
2988
2989        if (that.currentRequest) {
2990            that.currentRequest.abort();
2991        }
2992    },
2993
2994    /**
2995     * Checks response format and data
2996     * @return {Boolean} response contains acceptable data
2997     */
2998    processResponse: function(response) {
2999        var that = this,
3000            suggestions;
3001
3002        if (!response || !$.isArray(response.suggestions)) {
3003            return false;
3004        }
3005
3006        that.verifySuggestionsFormat(response.suggestions);
3007        that.setUnrestrictedValues(response.suggestions);
3008
3009        if ($.isFunction(that.options.onSuggestionsFetch)) {
3010            suggestions = that.options.onSuggestionsFetch.call(
3011                that.element,
3012                response.suggestions
3013            );
3014            if ($.isArray(suggestions)) {
3015                response.suggestions = suggestions;
3016            }
3017        }
3018
3019        return true;
3020    },
3021
3022    verifySuggestionsFormat: function(suggestions) {
3023        if (typeof suggestions[0] === "string") {
3024            $.each(suggestions, function(i, value) {
3025                suggestions[i] = { value: value, data: null };
3026            });
3027        }
3028    },
3029
3030    /**
3031     * Gets string to set as input value
3032     *
3033     * @param suggestion
3034     * @param {Object} [selectionOptions]
3035     * @param {boolean} selectionOptions.hasBeenEnriched
3036     * @param {boolean} selectionOptions.hasSameValues
3037     * @return {string}
3038     */
3039    getSuggestionValue: function(suggestion, selectionOptions) {
3040        var that = this,
3041            formatSelected =
3042                that.options.formatSelected || that.type.formatSelected,
3043            hasSameValues = selectionOptions && selectionOptions.hasSameValues,
3044            hasBeenEnriched =
3045                selectionOptions && selectionOptions.hasBeenEnriched,
3046            formattedValue,
3047            typeFormattedValue = null;
3048
3049        if ($.isFunction(formatSelected)) {
3050            formattedValue = formatSelected.call(that, suggestion);
3051        }
3052
3053        if (typeof formattedValue !== "string") {
3054            formattedValue = suggestion.value;
3055
3056            if (that.type.getSuggestionValue) {
3057                typeFormattedValue = that.type.getSuggestionValue(that, {
3058                    suggestion: suggestion,
3059                    hasSameValues: hasSameValues,
3060                    hasBeenEnriched: hasBeenEnriched
3061                });
3062
3063                if (typeFormattedValue !== null) {
3064                    formattedValue = typeFormattedValue;
3065                }
3066            }
3067        }
3068
3069        return formattedValue;
3070    },
3071
3072    hasSameValues: function(suggestion) {
3073        var hasSame = false;
3074
3075        $.each(this.suggestions, function(i, anotherSuggestion) {
3076            if (
3077                anotherSuggestion.value === suggestion.value &&
3078                anotherSuggestion !== suggestion
3079            ) {
3080                hasSame = true;
3081                return false;
3082            }
3083        });
3084
3085        return hasSame;
3086    },
3087
3088    assignSuggestions: function(suggestions, query) {
3089        var that = this;
3090        that.suggestions = suggestions;
3091        that.notify("assignSuggestions", query);
3092    },
3093
3094    shouldRestrictValues: function() {
3095        var that = this;
3096        // treat suggestions value as restricted only if there is one constraint
3097        // and restrict_value is true
3098        return (
3099            that.options.restrict_value &&
3100            that.constraints &&
3101            Object.keys(that.constraints).length == 1
3102        );
3103    },
3104
3105    /**
3106     * Fills suggestion.unrestricted_value property
3107     */
3108    setUnrestrictedValues: function(suggestions) {
3109        var that = this,
3110            shouldRestrict = that.shouldRestrictValues(),
3111            label = that.getFirstConstraintLabel();
3112
3113        $.each(suggestions, function(i, suggestion) {
3114            if (!suggestion.unrestricted_value) {
3115                suggestion.unrestricted_value = shouldRestrict
3116                    ? label + ", " + suggestion.value
3117                    : suggestion.value;
3118            }
3119        });
3120    },
3121
3122    areSuggestionsSame: function(a, b) {
3123        return a && b && a.value === b.value && utils.areSame(a.data, b.data);
3124    },
3125
3126    getNoSuggestionsHint: function() {
3127        var that = this;
3128        if (that.options.noSuggestionsHint === false) {
3129            return false;
3130        }
3131        return that.options.noSuggestionsHint || that.type.noSuggestionsHint;
3132    }
3133};
3134
3135/**
3136 * Methods related to INPUT's behavior
3137 */
3138
3139var methods = {
3140    setupElement: function() {
3141        // Remove autocomplete attribute to prevent native suggestions:
3142        this.el
3143            // if it stops working, see https://stackoverflow.com/q/15738259
3144            // chrome is constantly changing this logic
3145            .attr("autocomplete", "new-password")
3146            .attr("autocorrect", "off")
3147            .attr("autocapitalize", "off")
3148            .attr("spellcheck", "false")
3149            .addClass("suggestions-input")
3150            .css("box-sizing", "border-box");
3151    },
3152
3153    bindElementEvents: function() {
3154        var that = this;
3155
3156        that.el.on("keydown" + EVENT_NS, $.proxy(that.onElementKeyDown, that));
3157        // IE is buggy, it doesn't trigger `input` on text deletion, so use following events
3158        that.el.on(
3159            [
3160                "keyup" + EVENT_NS,
3161                "cut" + EVENT_NS,
3162                "paste" + EVENT_NS,
3163                "input" + EVENT_NS
3164            ].join(" "),
3165            $.proxy(that.onElementKeyUp, that)
3166        );
3167        that.el.on("blur" + EVENT_NS, $.proxy(that.onElementBlur, that));
3168        that.el.on("focus" + EVENT_NS, $.proxy(that.onElementFocus, that));
3169    },
3170
3171    unbindElementEvents: function() {
3172        this.el.off(EVENT_NS);
3173    },
3174
3175    onElementBlur: function() {
3176        var that = this;
3177
3178        // suggestion was clicked, blur should be ignored
3179        // see container mousedown handler
3180        if (that.cancelBlur) {
3181            that.cancelBlur = false;
3182            return;
3183        }
3184
3185        if (that.options.triggerSelectOnBlur) {
3186            if (!that.isUnavailable()) {
3187                that.selectCurrentValue({ noSpace: true }).always(function() {
3188                    // For NAMEs selecting keeps suggestions list visible, so hide it
3189                    that.hide();
3190                });
3191            }
3192        } else {
3193            that.hide();
3194        }
3195
3196        if (that.fetchPhase.abort) {
3197            that.fetchPhase.abort();
3198        }
3199    },
3200
3201    onElementFocus: function() {
3202        var that = this;
3203
3204        if (!that.cancelFocus) {
3205            // defer methods to allow browser update input's style before
3206            utils.delay($.proxy(that.completeOnFocus, that));
3207        }
3208        that.cancelFocus = false;
3209    },
3210
3211    onElementKeyDown: function(e) {
3212        var that = this;
3213
3214        if (that.isUnavailable()) {
3215            return;
3216        }
3217
3218        if (!that.visible) {
3219            switch (e.which) {
3220                // If suggestions are hidden and user presses arrow down, display suggestions
3221                case KEYS.DOWN:
3222                    that.suggest();
3223                    break;
3224                // if no suggestions available and user pressed Enter
3225                case KEYS.ENTER:
3226                    if (that.options.triggerSelectOnEnter) {
3227                        that.triggerOnSelectNothing();
3228                    }
3229                    break;
3230            }
3231            return;
3232        }
3233
3234        switch (e.which) {
3235            case KEYS.ESC:
3236                that.el.val(that.currentValue);
3237                that.hide();
3238                that.abortRequest();
3239                break;
3240
3241            case KEYS.TAB:
3242                if (that.options.tabDisabled === false) {
3243                    return;
3244                }
3245                break;
3246
3247            case KEYS.ENTER:
3248                if (that.options.triggerSelectOnEnter) {
3249                    that.selectCurrentValue();
3250                }
3251                break;
3252
3253            case KEYS.SPACE:
3254                if (that.options.triggerSelectOnSpace && that.isCursorAtEnd()) {
3255                    e.preventDefault();
3256                    that.selectCurrentValue({
3257                        continueSelecting: true,
3258                        dontEnrich: true
3259                    }).fail(function() {
3260                        // If all data fetched but nothing selected
3261                        that.currentValue += " ";
3262                        that.el.val(that.currentValue);
3263                        that.proceedChangedValue();
3264                    });
3265                }
3266                return;
3267            case KEYS.UP:
3268                that.moveUp();
3269                break;
3270            case KEYS.DOWN:
3271                that.moveDown();
3272                break;
3273            default:
3274                return;
3275        }
3276
3277        // Cancel event if function did not return:
3278        e.stopImmediatePropagation();
3279        e.preventDefault();
3280    },
3281
3282    onElementKeyUp: function(e) {
3283        var that = this;
3284
3285        if (that.isUnavailable()) {
3286            return;
3287        }
3288
3289        switch (e.which) {
3290            case KEYS.UP:
3291            case KEYS.DOWN:
3292            case KEYS.ENTER:
3293                return;
3294        }
3295
3296        // Cancel pending change
3297        clearTimeout(that.onChangeTimeout);
3298        that.inputPhase.reject();
3299
3300        if (that.currentValue !== that.el.val()) {
3301            that.proceedChangedValue();
3302        }
3303    },
3304
3305    proceedChangedValue: function() {
3306        var that = this;
3307
3308        // Cancel fetching, because it became obsolete
3309        that.abortRequest();
3310
3311        that.inputPhase = $.Deferred().done($.proxy(that.onValueChange, that));
3312
3313        if (that.options.deferRequestBy > 0) {
3314            // Defer lookup in case when value changes very quickly:
3315            that.onChangeTimeout = utils.delay(function() {
3316                that.inputPhase.resolve();
3317            }, that.options.deferRequestBy);
3318        } else {
3319            that.inputPhase.resolve();
3320        }
3321    },
3322
3323    onValueChange: function() {
3324        var that = this,
3325            currentSelection;
3326
3327        if (that.selection) {
3328            currentSelection = that.selection;
3329            that.selection = null;
3330            that.trigger("InvalidateSelection", currentSelection);
3331        }
3332
3333        that.selectedIndex = -1;
3334
3335        that.update();
3336        that.notify("valueChange");
3337    },
3338
3339    completeOnFocus: function() {
3340        var that = this;
3341
3342        if (that.isUnavailable()) {
3343            return;
3344        }
3345
3346        if (that.isElementFocused()) {
3347            that.update();
3348            if (that.isMobile) {
3349                that.setCursorAtEnd();
3350                that.scrollToTop();
3351            }
3352        }
3353    },
3354
3355    isElementFocused: function() {
3356        return document.activeElement === this.element;
3357    },
3358
3359    isElementDisabled: function() {
3360        return Boolean(
3361            this.element.getAttribute("disabled") ||
3362                this.element.getAttribute("readonly")
3363        );
3364    },
3365
3366    isCursorAtEnd: function() {
3367        var that = this,
3368            valLength = that.el.val().length,
3369            selectionStart,
3370            range;
3371
3372        // `selectionStart` and `selectionEnd` are not supported by some input types
3373        try {
3374            selectionStart = that.element.selectionStart;
3375            if (typeof selectionStart === "number") {
3376                return selectionStart === valLength;
3377            }
3378        } catch (ex) {}
3379
3380        if (document.selection) {
3381            range = document.selection.createRange();
3382            range.moveStart("character", -valLength);
3383            return valLength === range.text.length;
3384        }
3385        return true;
3386    },
3387
3388    setCursorAtEnd: function() {
3389        var element = this.element;
3390
3391        // `selectionStart` and `selectionEnd` are not supported by some input types
3392        try {
3393            element.selectionEnd = element.selectionStart =
3394                element.value.length;
3395            element.scrollLeft = element.scrollWidth;
3396        } catch (ex) {
3397            element.value = element.value;
3398        }
3399    }
3400};
3401
3402$.extend(Suggestions.prototype, methods);
3403
3404notificator
3405    .on("initialize", methods.bindElementEvents)
3406    .on("dispose", methods.unbindElementEvents);
3407
3408/**
3409 * Methods related to plugin's authorization on server
3410 */
3411
3412// keys are "[type][token]"
3413var statusRequests = {};
3414
3415function resetTokens() {
3416    utils.each(statusRequests, function(req) {
3417        req.abort();
3418    });
3419    statusRequests = {};
3420}
3421
3422resetTokens();
3423
3424var methods$1 = {
3425    checkStatus: function() {
3426        var that = this,
3427            token = (that.options.token && that.options.token.trim()) || "",
3428            requestKey = that.options.type + token,
3429            request = statusRequests[requestKey];
3430
3431        if (!request) {
3432            request = statusRequests[requestKey] = jqapi.ajax(
3433                that.getAjaxParams("status")
3434            );
3435        }
3436
3437        request
3438            .done(function(status, textStatus, request) {
3439                if (status.search) {
3440                    var plan = request.getResponseHeader("X-Plan");
3441                    status.plan = plan;
3442                    jqapi.extend(that.status, status);
3443                } else {
3444                    triggerError("Service Unavailable");
3445                }
3446            })
3447            .fail(function() {
3448                triggerError(request.statusText);
3449            });
3450
3451        function triggerError(errorThrown) {
3452            // If unauthorized
3453            if (utils.isFunction(that.options.onSearchError)) {
3454                that.options.onSearchError.call(
3455                    that.element,
3456                    null,
3457                    request,
3458                    "error",
3459                    errorThrown
3460                );
3461            }
3462        }
3463    }
3464};
3465
3466Suggestions.resetTokens = resetTokens;
3467
3468jqapi.extend(Suggestions.prototype, methods$1);
3469
3470notificator.on("setOptions", methods$1.checkStatus);
3471
3472//export { methods, resetTokens };
3473
3474var locationRequest;
3475var defaultGeoLocation = true;
3476
3477function resetLocation() {
3478    locationRequest = null;
3479    DEFAULT_OPTIONS.geoLocation = defaultGeoLocation;
3480}
3481
3482var methods$2 = {
3483    checkLocation: function() {
3484        var that = this,
3485            providedLocation = that.options.geoLocation;
3486
3487        if (!that.type.geoEnabled || !providedLocation) {
3488            return;
3489        }
3490
3491        that.geoLocation = $.Deferred();
3492        if ($.isPlainObject(providedLocation) || $.isArray(providedLocation)) {
3493            that.geoLocation.resolve(providedLocation);
3494        } else {
3495            if (!locationRequest) {
3496                locationRequest = $.ajax(
3497                    that.getAjaxParams("iplocate/address")
3498                );
3499            }
3500
3501            locationRequest
3502                .done(function(resp) {
3503                    var locationData =
3504                        resp && resp.location && resp.location.data;
3505                    if (locationData && locationData.kladr_id) {
3506                        that.geoLocation.resolve({
3507                            kladr_id: locationData.kladr_id
3508                        });
3509                    } else {
3510                        that.geoLocation.reject();
3511                    }
3512                })
3513                .fail(function() {
3514                    that.geoLocation.reject();
3515                });
3516        }
3517    },
3518
3519    /**
3520     * Public method to get `geoLocation` promise
3521     * @returns {$.Deferred}
3522     */
3523    getGeoLocation: function() {
3524        return this.geoLocation;
3525    },
3526
3527    constructParams: function() {
3528        var that = this,
3529            params = {};
3530
3531        if (
3532            that.geoLocation &&
3533            $.isFunction(that.geoLocation.promise) &&
3534            that.geoLocation.state() == "resolved"
3535        ) {
3536            that.geoLocation.done(function(locationData) {
3537                params["locations_boost"] = $.makeArray(locationData);
3538            });
3539        }
3540
3541        return params;
3542    }
3543};
3544
3545// Disable this feature when GET method used. See SUG-202
3546if (utils.getDefaultType() != "GET") {
3547    $.extend(DEFAULT_OPTIONS, {
3548        geoLocation: defaultGeoLocation
3549    });
3550
3551    $.extend(Suggestions, {
3552        resetLocation: resetLocation
3553    });
3554
3555    $.extend(Suggestions.prototype, {
3556        getGeoLocation: methods$2.getGeoLocation
3557    });
3558
3559    notificator
3560        .on("setOptions", methods$2.checkLocation)
3561        .on("requestParams", methods$2.constructParams);
3562}
3563
3564var methods$3 = {
3565    enrichSuggestion: function(suggestion, selectionOptions) {
3566        var that = this,
3567            resolver = $.Deferred();
3568
3569        if (
3570            !that.options.enrichmentEnabled ||
3571            !that.type.enrichmentEnabled ||
3572            !that.requestMode.enrichmentEnabled ||
3573            (selectionOptions && selectionOptions.dontEnrich)
3574        ) {
3575            return resolver.resolve(suggestion);
3576        }
3577
3578        // if current suggestion is already enriched, use it
3579        if (suggestion.data && suggestion.data.qc != null) {
3580            return resolver.resolve(suggestion);
3581        }
3582
3583        that.disableDropdown();
3584
3585        var query = that.type.getEnrichmentQuery(suggestion);
3586        var customParams = that.type.enrichmentParams;
3587        var requestOptions = {
3588            noCallbacks: true,
3589            useEnrichmentCache: true,
3590            method: that.type.enrichmentMethod
3591        };
3592
3593        // Set `currentValue` to make `processResponse` to consider enrichment response valid
3594        that.currentValue = query;
3595
3596        // prevent request abortion during onBlur
3597        that.enrichPhase = that
3598            .getSuggestions(query, customParams, requestOptions)
3599            .always(function() {
3600                that.enableDropdown();
3601            })
3602            .done(function(suggestions) {
3603                var enrichedSuggestion = suggestions && suggestions[0];
3604
3605                resolver.resolve(
3606                    enrichedSuggestion || suggestion,
3607                    !!enrichedSuggestion
3608                );
3609            })
3610            .fail(function() {
3611                resolver.resolve(suggestion);
3612            });
3613
3614        return resolver;
3615    },
3616
3617    /**
3618     * Injects enriched suggestion into response
3619     * @param response
3620     * @param query
3621     */
3622    enrichResponse: function(response, query) {
3623        var that = this,
3624            enrichedSuggestion = that.enrichmentCache[query];
3625
3626        if (enrichedSuggestion) {
3627            $.each(response.suggestions, function(i, suggestion) {
3628                if (suggestion.value === query) {
3629                    response.suggestions[i] = enrichedSuggestion;
3630                    return false;
3631                }
3632            });
3633        }
3634    }
3635};
3636
3637$.extend(Suggestions.prototype, methods$3);
3638
3639/**
3640 * Methods related to suggestions dropdown list
3641 */
3642
3643function highlightMatches(chunks) {
3644    return $.map(chunks, function(chunk) {
3645        var text = utils.escapeHtml(chunk.text);
3646
3647        if (text && chunk.matched) {
3648            text = "<strong>" + text + "</strong>";
3649        }
3650        return text;
3651    }).join("");
3652}
3653
3654function nowrapLinkedParts(formattedStr, nowrapClass) {
3655    var delimitedParts = formattedStr.split(", ");
3656    // string has no delimiters, should not wrap
3657    if (delimitedParts.length === 1) {
3658        return formattedStr;
3659    }
3660    // disable word-wrap inside delimited parts
3661    return $.map(delimitedParts, function(part) {
3662        return '<span class="' + nowrapClass + '">' + part + "</span>";
3663    }).join(", ");
3664}
3665
3666function hasAnotherSuggestion(suggestions, suggestion) {
3667    var result = false;
3668
3669    $.each(suggestions, function(i, s) {
3670        result = s.value == suggestion.value && s != suggestion;
3671        if (result) {
3672            return false;
3673        }
3674    });
3675
3676    return result;
3677}
3678
3679var optionsUsed = {
3680    width: "auto",
3681    floating: false
3682};
3683
3684var methods$4 = {
3685    createContainer: function() {
3686        var that = this,
3687            suggestionSelector = "." + that.classes.suggestion,
3688            options = that.options,
3689            $container = $("<div/>")
3690                .addClass(options.containerClass)
3691                .css({
3692                    display: "none"
3693                });
3694
3695        that.$container = $container;
3696
3697        $container.on(
3698            "click" + EVENT_NS,
3699            suggestionSelector,
3700            $.proxy(that.onSuggestionClick, that)
3701        );
3702    },
3703
3704    showContainer: function() {
3705        this.$container.appendTo(
3706            this.options.floating ? this.$body : this.$wrapper
3707        );
3708    },
3709
3710    getContainer: function() {
3711        return this.$container.get(0);
3712    },
3713
3714    removeContainer: function() {
3715        var that = this;
3716
3717        if (that.options.floating) {
3718            that.$container.remove();
3719        }
3720    },
3721
3722    setContainerOptions: function() {
3723        var that = this,
3724            mousedownEvent = "mousedown" + EVENT_NS;
3725
3726        that.$container.off(mousedownEvent);
3727        if (that.options.floating) {
3728            that.$container.on(mousedownEvent, $.proxy(that.onMousedown, that));
3729        }
3730    },
3731
3732    /**
3733     * Listen for click event on suggestions list:
3734     */
3735    onSuggestionClick: function(e) {
3736        var that = this,
3737            $el = $(e.target),
3738            index;
3739
3740        if (!that.dropdownDisabled) {
3741            that.cancelFocus = true;
3742            that.el.focus();
3743
3744            while ($el.length && !(index = $el.attr("data-index"))) {
3745                $el = $el.closest("." + that.classes.suggestion);
3746            }
3747
3748            if (index && !isNaN(index)) {
3749                that.select(+index);
3750            }
3751        }
3752    },
3753
3754    // Dropdown UI methods
3755
3756    getSuggestionsItems: function() {
3757        return this.$container.children("." + this.classes.suggestion);
3758    },
3759
3760    toggleDropdownEnabling: function(enable) {
3761        this.dropdownDisabled = !enable;
3762        this.$container.attr("disabled", !enable);
3763    },
3764
3765    disableDropdown: function() {
3766        this.toggleDropdownEnabling(false);
3767    },
3768
3769    enableDropdown: function() {
3770        this.toggleDropdownEnabling(true);
3771    },
3772
3773    /**
3774     * Shows if there are any suggestions besides currently selected
3775     * @returns {boolean}
3776     */
3777    hasSuggestionsToChoose: function() {
3778        var that = this;
3779
3780        return (
3781            that.suggestions.length > 1 ||
3782            (that.suggestions.length === 1 &&
3783                (!that.selection ||
3784                    $.trim(that.suggestions[0].value) !==
3785                        $.trim(that.selection.value)))
3786        );
3787    },
3788
3789    suggest: function() {
3790        var that = this,
3791            options = that.options,
3792            formatResult,
3793            html = [];
3794
3795        if (!that.requestMode.userSelect) {
3796            return;
3797        }
3798
3799        // если нечего показывать, то сообщаем об этом
3800        if (!that.hasSuggestionsToChoose()) {
3801            if (that.suggestions.length) {
3802                that.hide();
3803                return;
3804            } else {
3805                var noSuggestionsHint = that.getNoSuggestionsHint();
3806                if (noSuggestionsHint) {
3807                    html.push(
3808                        '<div class="' +
3809                            that.classes.hint +
3810                            '">' +
3811                            noSuggestionsHint +
3812                            "</div>"
3813                    );
3814                } else {
3815                    that.hide();
3816                    return;
3817                }
3818            }
3819        } else {
3820            // Build hint html
3821            if (options.hint && that.suggestions.length) {
3822                html.push(
3823                    '<div class="' +
3824                        that.classes.hint +
3825                        '">' +
3826                        options.hint +
3827                        "</div>"
3828                );
3829            }
3830            that.selectedIndex = -1;
3831            // Build suggestions inner HTML:
3832            that.suggestions.forEach(function(suggestion, i) {
3833                if (suggestion == that.selection) {
3834                    that.selectedIndex = i;
3835                }
3836                that.buildSuggestionHtml(suggestion, i, html);
3837            });
3838        }
3839
3840        html.push('<div class="' + CLASSES.promo + '"></div>');
3841        html.push("</div>");
3842
3843        that.$container.html(html.join(""));
3844
3845        // Select first value by default:
3846        if (options.autoSelectFirst && that.selectedIndex === -1) {
3847            that.selectedIndex = 0;
3848        }
3849        if (that.selectedIndex !== -1) {
3850            that.getSuggestionsItems()
3851                .eq(that.selectedIndex)
3852                .addClass(that.classes.selected);
3853        }
3854
3855        if ($.isFunction(options.beforeRender)) {
3856            options.beforeRender.call(that.element, that.$container);
3857        }
3858
3859        that.$container.show();
3860        that.visible = true;
3861    },
3862
3863    buildSuggestionHtml: function(suggestion, ordinal, html) {
3864        html.push(
3865            '<div class="' +
3866                this.classes.suggestion +
3867                '" data-index="' +
3868                ordinal +
3869                '">'
3870        );
3871
3872        var formatResult =
3873            this.options.formatResult ||
3874            this.type.formatResult ||
3875            this.formatResult;
3876        html.push(
3877            formatResult.call(
3878                this,
3879                suggestion.value,
3880                this.currentValue,
3881                suggestion,
3882                {
3883                    unformattableTokens: this.type.unformattableTokens
3884                }
3885            )
3886        );
3887
3888        var labels = this.makeSuggestionLabel(this.suggestions, suggestion);
3889        if (labels) {
3890            html.push(
3891                '<span class="' +
3892                    this.classes.subtext_label +
3893                    '">' +
3894                    utils.escapeHtml(labels) +
3895                    "</span>"
3896            );
3897        }
3898        html.push("</div>");
3899    },
3900
3901    wrapFormattedValue: function(value, suggestion) {
3902        var that = this,
3903            status = utils.getDeepValue(suggestion.data, "state.status");
3904
3905        return (
3906            '<span class="' +
3907            that.classes.value +
3908            '"' +
3909            (status ? ' data-suggestion-status="' + status + '"' : "") +
3910            ">" +
3911            value +
3912            "</span>"
3913        );
3914    },
3915
3916    formatResult: function(value, currentValue, suggestion, options) {
3917        var that = this;
3918
3919        value = that.highlightMatches(value, currentValue, suggestion, options);
3920
3921        return that.wrapFormattedValue(value, suggestion);
3922    },
3923
3924    /**
3925     * Makes HTML contents for suggestion item
3926     * @param {String} value string to be displayed as a value
3927     * @param {String} currentValue contents of the textbox
3928     * @param suggestion whole suggestion object with displaying value and other fields
3929     * @param {Object} [options] set of flags:
3930     *          `unformattableTokens` - array of search tokens, that are not to be highlighted
3931     *          `maxLength` - if set, `value` is limited by this length
3932     * @returns {String} HTML to be inserted in the list
3933     */
3934    highlightMatches: function(value, currentValue, suggestion, options) {
3935        var that = this,
3936            chunks = [],
3937            unformattableTokens = options && options.unformattableTokens,
3938            maxLength = options && options.maxLength,
3939            tokens,
3940            tokenMatchers,
3941            preferredTokens,
3942            rWords = utils.reWordExtractor(),
3943            match,
3944            word,
3945            i,
3946            chunk,
3947            formattedStr;
3948
3949        if (!value) return "";
3950
3951        tokens = text_util.tokenize(currentValue, unformattableTokens);
3952
3953        tokenMatchers = $.map(tokens, function(token) {
3954            return new RegExp(
3955                "^((.*)([" +
3956                    WORD_PARTS_DELIMITERS +
3957                    "]+))?" +
3958                    "(" +
3959                    utils.escapeRegExChars(token) +
3960                    ")" +
3961                    "([^" +
3962                    WORD_PARTS_DELIMITERS +
3963                    "]*[" +
3964                    WORD_PARTS_DELIMITERS +
3965                    "]*)",
3966                "i"
3967            );
3968        });
3969
3970        // parse string by words
3971        while ((match = rWords.exec(value)) && match[0]) {
3972            word = match[1];
3973            chunks.push({
3974                text: word,
3975
3976                // upper case means a word is a name and can be highlighted even if presents in unformattableTokens
3977                hasUpperCase: word.toLowerCase() !== word,
3978                formatted: utils.formatToken(word),
3979                matchable: true
3980            });
3981            if (match[2]) {
3982                chunks.push({
3983                    text: match[2]
3984                });
3985            }
3986        }
3987
3988        // use simple loop because length can change
3989        for (i = 0; i < chunks.length; i++) {
3990            chunk = chunks[i];
3991            if (
3992                chunk.matchable &&
3993                !chunk.matched &&
3994                ($.inArray(chunk.formatted, unformattableTokens) === -1 ||
3995                    chunk.hasUpperCase)
3996            ) {
3997                $.each(tokenMatchers, function(j, matcher) {
3998                    var tokenMatch = matcher.exec(chunk.formatted),
3999                        length,
4000                        nextIndex = i + 1;
4001
4002                    if (tokenMatch) {
4003                        tokenMatch = {
4004                            before: tokenMatch[1] || "",
4005                            beforeText: tokenMatch[2] || "",
4006                            beforeDelimiter: tokenMatch[3] || "",
4007                            text: tokenMatch[4] || "",
4008                            after: tokenMatch[5] || ""
4009                        };
4010
4011                        if (tokenMatch.before) {
4012                            // insert chunk before current
4013                            chunks.splice(
4014                                i,
4015                                0,
4016                                {
4017                                    text: chunk.text.substr(
4018                                        0,
4019                                        tokenMatch.beforeText.length
4020                                    ),
4021                                    formatted: tokenMatch.beforeText,
4022                                    matchable: true
4023                                },
4024                                {
4025                                    text: tokenMatch.beforeDelimiter
4026                                }
4027                            );
4028                            nextIndex += 2;
4029
4030                            length = tokenMatch.before.length;
4031                            chunk.text = chunk.text.substr(length);
4032                            chunk.formatted = chunk.formatted.substr(length);
4033                            i--;
4034                        }
4035
4036                        length =
4037                            tokenMatch.text.length + tokenMatch.after.length;
4038                        if (chunk.formatted.length > length) {
4039                            chunks.splice(nextIndex, 0, {
4040                                text: chunk.text.substr(length),
4041                                formatted: chunk.formatted.substr(length),
4042                                matchable: true
4043                            });
4044                            chunk.text = chunk.text.substr(0, length);
4045                            chunk.formatted = chunk.formatted.substr(0, length);
4046                        }
4047
4048                        if (tokenMatch.after) {
4049                            length = tokenMatch.text.length;
4050                            chunks.splice(nextIndex, 0, {
4051                                text: chunk.text.substr(length),
4052                                formatted: chunk.formatted.substr(length)
4053                            });
4054                            chunk.text = chunk.text.substr(0, length);
4055                            chunk.formatted = chunk.formatted.substr(0, length);
4056                        }
4057                        chunk.matched = true;
4058                        return false;
4059                    }
4060                });
4061            }
4062        }
4063
4064        if (maxLength) {
4065            for (i = 0; i < chunks.length && maxLength >= 0; i++) {
4066                chunk = chunks[i];
4067                maxLength -= chunk.text.length;
4068                if (maxLength < 0) {
4069                    chunk.text =
4070                        chunk.text.substr(0, chunk.text.length + maxLength) +
4071                        "...";
4072                }
4073            }
4074            chunks.length = i;
4075        }
4076
4077        formattedStr = highlightMatches(chunks);
4078        return nowrapLinkedParts(formattedStr, that.classes.nowrap);
4079    },
4080
4081    makeSuggestionLabel: function(suggestions, suggestion) {
4082        var that = this,
4083            fieldNames = that.type.fieldNames,
4084            nameData = {},
4085            rWords = utils.reWordExtractor(),
4086            match,
4087            word,
4088            labels = [];
4089
4090        if (
4091            fieldNames &&
4092            hasAnotherSuggestion(suggestions, suggestion) &&
4093            suggestion.data
4094        ) {
4095            $.each(fieldNames, function(field) {
4096                var value = suggestion.data[field];
4097                if (value) {
4098                    nameData[field] = utils.formatToken(value);
4099                }
4100            });
4101
4102            if (!$.isEmptyObject(nameData)) {
4103                while (
4104                    (match = rWords.exec(
4105                        utils.formatToken(suggestion.value)
4106                    )) &&
4107                    (word = match[1])
4108                ) {
4109                    $.each(nameData, function(i, value) {
4110                        if (value == word) {
4111                            labels.push(fieldNames[i]);
4112                            delete nameData[i];
4113                            return false;
4114                        }
4115                    });
4116                }
4117
4118                if (labels.length) {
4119                    return labels.join(", ");
4120                }
4121            }
4122        }
4123    },
4124
4125    hide: function() {
4126        var that = this;
4127        that.visible = false;
4128        that.selectedIndex = -1;
4129        that.$container.hide().empty();
4130    },
4131
4132    activate: function(index) {
4133        var that = this,
4134            $activeItem,
4135            selected = that.classes.selected,
4136            $children;
4137
4138        if (!that.dropdownDisabled) {
4139            $children = that.getSuggestionsItems();
4140
4141            $children.removeClass(selected);
4142
4143            that.selectedIndex = index;
4144
4145            if (
4146                that.selectedIndex !== -1 &&
4147                $children.length > that.selectedIndex
4148            ) {
4149                $activeItem = $children.eq(that.selectedIndex);
4150                $activeItem.addClass(selected);
4151                return $activeItem;
4152            }
4153        }
4154
4155        return null;
4156    },
4157
4158    deactivate: function(restoreValue) {
4159        var that = this;
4160
4161        if (!that.dropdownDisabled) {
4162            that.selectedIndex = -1;
4163            that.getSuggestionsItems().removeClass(that.classes.selected);
4164            if (restoreValue) {
4165                that.el.val(that.currentValue);
4166            }
4167        }
4168    },
4169
4170    moveUp: function() {
4171        var that = this;
4172
4173        if (that.dropdownDisabled) {
4174            return;
4175        }
4176        if (that.selectedIndex === -1) {
4177            if (that.suggestions.length) {
4178                that.adjustScroll(that.suggestions.length - 1);
4179            }
4180            return;
4181        }
4182
4183        if (that.selectedIndex === 0) {
4184            that.deactivate(true);
4185            return;
4186        }
4187
4188        that.adjustScroll(that.selectedIndex - 1);
4189    },
4190
4191    moveDown: function() {
4192        var that = this;
4193
4194        if (that.dropdownDisabled) {
4195            return;
4196        }
4197        if (that.selectedIndex === that.suggestions.length - 1) {
4198            that.deactivate(true);
4199            return;
4200        }
4201
4202        that.adjustScroll(that.selectedIndex + 1);
4203    },
4204
4205    adjustScroll: function(index) {
4206        var that = this,
4207            $activeItem = that.activate(index),
4208            itemTop,
4209            itemBottom,
4210            scrollTop = that.$container.scrollTop(),
4211            containerHeight;
4212
4213        if (!$activeItem || !$activeItem.length) {
4214            return;
4215        }
4216
4217        itemTop = $activeItem.position().top;
4218        if (itemTop < 0) {
4219            that.$container.scrollTop(scrollTop + itemTop);
4220        } else {
4221            itemBottom = itemTop + $activeItem.outerHeight();
4222            containerHeight = that.$container.innerHeight();
4223            if (itemBottom > containerHeight) {
4224                that.$container.scrollTop(
4225                    scrollTop - containerHeight + itemBottom
4226                );
4227            }
4228        }
4229
4230        that.el.val(that.suggestions[index].value);
4231    }
4232};
4233
4234$.extend(DEFAULT_OPTIONS, optionsUsed);
4235
4236$.extend(Suggestions.prototype, methods$4);
4237
4238notificator
4239    .on("initialize", methods$4.createContainer)
4240    .on("dispose", methods$4.removeContainer)
4241    .on("setOptions", methods$4.setContainerOptions)
4242    .on("ready", methods$4.showContainer)
4243    .on("assignSuggestions", methods$4.suggest);
4244
4245/**
4246 * Methods related to CONSTRAINTS component
4247 */
4248var optionsUsed$1 = {
4249    constraints: null,
4250    restrict_value: false
4251};
4252
4253var fiasParamNames = [
4254    "country_iso_code",
4255    "region_iso_code",
4256    "region_fias_id",
4257    "area_fias_id",
4258    "city_fias_id",
4259    "city_district_fias_id",
4260    "settlement_fias_id",
4261    "planning_structure_fias_id",
4262    "street_fias_id"
4263];
4264
4265/**
4266 * Compares two suggestion objects
4267 * @param suggestion
4268 * @param instance other Suggestions instance
4269 */
4270function belongsToArea(suggestion, instance) {
4271    var parentSuggestion = instance.selection,
4272        result = parentSuggestion && parentSuggestion.data && instance.bounds;
4273
4274    if (result) {
4275        collection_util.each(instance.bounds.all, function(bound, i) {
4276            return (result =
4277                parentSuggestion.data[bound] === suggestion.data[bound]);
4278        });
4279    }
4280    return result;
4281}
4282
4283/**
4284 * Возвращает КЛАДР-код, обрезанный до последнего непустого уровня
4285 * 50 000 040 000 00 → 50 000 040
4286 * @param kladr_id
4287 * @returns {string}
4288 */
4289function getSignificantKladrId(kladr_id) {
4290    var significantKladrId = kladr_id.replace(/^(\d{2})(\d*?)(0+)$/g, "$1$2");
4291    var length = significantKladrId.length;
4292    var significantLength = -1;
4293    if (length <= 2) {
4294        significantLength = 2;
4295    } else if (length > 2 && length <= 5) {
4296        significantLength = 5;
4297    } else if (length > 5 && length <= 8) {
4298        significantLength = 8;
4299    } else if (length > 8 && length <= 11) {
4300        significantLength = 11;
4301    } else if (length > 11 && length <= 15) {
4302        significantLength = 15;
4303    } else if (length > 15) {
4304        significantLength = 19;
4305    }
4306    return text_util.padEnd(significantKladrId, significantLength, "0");
4307}
4308
4309/**
4310 * @param {Object} data  fields
4311 * @param {Suggestions} instance
4312 * @constructor
4313 */
4314var ConstraintLocation = function(data, instance) {
4315    var that = this,
4316        fieldNames,
4317        fiasFieldNames,
4318        fiasFields = {};
4319
4320    that.instance = instance;
4321    that.fields = {};
4322    that.specificity = -1;
4323
4324    if (lang_util.isPlainObject(data) && instance.type.dataComponents) {
4325        collection_util.each(instance.type.dataComponents, function(
4326            component,
4327            i
4328        ) {
4329            var fieldName = component.id;
4330
4331            if (component.forLocations && data[fieldName]) {
4332                that.fields[fieldName] = data[fieldName];
4333                that.specificity = i;
4334            }
4335        });
4336    }
4337
4338    fieldNames = Object.keys(that.fields);
4339    fiasFieldNames = collection_util.intersect(fieldNames, fiasParamNames);
4340    if (fiasFieldNames.length) {
4341        collection_util.each(fiasFieldNames, function(fieldName, index) {
4342            fiasFields[fieldName] = that.fields[fieldName];
4343        });
4344        that.fields = fiasFields;
4345        that.specificity = that.getFiasSpecificity(fiasFieldNames);
4346    } else if (that.fields.kladr_id) {
4347        that.fields = { kladr_id: that.fields.kladr_id };
4348        that.significantKladr = getSignificantKladrId(that.fields.kladr_id);
4349        that.specificity = that.getKladrSpecificity(that.significantKladr);
4350    }
4351};
4352
4353jqapi.extend(ConstraintLocation.prototype, {
4354    getLabel: function() {
4355        return this.instance.type.composeValue(this.fields, {
4356            saveCityDistrict: true
4357        });
4358    },
4359
4360    getFields: function() {
4361        return this.fields;
4362    },
4363
4364    isValid: function() {
4365        return !lang_util.isEmptyObject(this.fields);
4366    },
4367
4368    /**
4369     * Возвращает specificity для КЛАДР
4370     * Описание ниже, в getFiasSpecificity
4371     * @param kladr_id
4372     * @returns {number}
4373     */
4374    getKladrSpecificity: function(kladr_id) {
4375        var specificity = -1;
4376        var kladrLength = kladr_id.length;
4377
4378        collection_util.each(this.instance.type.dataComponents, function(
4379            component,
4380            i
4381        ) {
4382            if (
4383                component.kladrFormat &&
4384                kladrLength === component.kladrFormat.digits
4385            ) {
4386                specificity = i;
4387            }
4388        });
4389
4390        return specificity;
4391    },
4392
4393    /**
4394     * Возвращает особую величину specificity для ФИАС
4395     * Specificity это индекс для массива this.instance.type.dataComponents
4396     * до которого (включительно) обрежется этот массив при формировании строки адреса.
4397     * Этот параметр нужен для случаев, когда в настройках плагина restrict_value = true
4398     * Например, установлено ограничение (locations) по region_fias_id (Краснодарский край)
4399     * В выпадашке нажимаем на "г. Сочи"
4400     * Если restrict_value отключен, то выведется значение "Краснодарский край, г Сочи"
4401     * Если включен, то просто "г Сочи"
4402     *
4403     * @param fiasFieldNames
4404     * @returns {number}
4405     */
4406    getFiasSpecificity: function(fiasFieldNames) {
4407        var specificity = -1;
4408
4409        collection_util.each(this.instance.type.dataComponents, function(
4410            component,
4411            i
4412        ) {
4413            if (
4414                component.fiasType &&
4415                fiasFieldNames.indexOf(component.fiasType) > -1 &&
4416                specificity < i
4417            ) {
4418                specificity = i;
4419            }
4420        });
4421
4422        return specificity;
4423    },
4424
4425    containsData: function(data) {
4426        var result = true;
4427
4428        if (this.fields.kladr_id) {
4429            return (
4430                !!data.kladr_id &&
4431                data.kladr_id.indexOf(this.significantKladr) === 0
4432            );
4433        } else {
4434            collection_util.each(this.fields, function(value, fieldName) {
4435                return (result =
4436                    !!data[fieldName] &&
4437                    data[fieldName].toLowerCase() === value.toLowerCase());
4438            });
4439
4440            return result;
4441        }
4442    }
4443});
4444
4445Suggestions.ConstraintLocation = ConstraintLocation;
4446
4447/**
4448 * @param {Object} data
4449 * @param {Object|Array} data.locations
4450 * @param {string} [data.label]
4451 * @param {boolean} [data.deletable]
4452 * @param {Suggestions} [instance]
4453 * @constructor
4454 */
4455var Constraint = function(data, instance) {
4456    this.id = generateId("c");
4457    this.deletable = !!data.deletable;
4458    this.instance = instance;
4459
4460    var locationsArray = collection_util.makeArray(
4461        data && (data.locations || data.restrictions)
4462    );
4463    this.locations = locationsArray.map(function(data) {
4464        return new ConstraintLocation(data, instance);
4465    });
4466
4467    this.locations = this.locations.filter(function(location) {
4468        return location.isValid();
4469    });
4470
4471    this.label = data.label;
4472    if (this.label == null && instance.type.composeValue) {
4473        this.label = this.locations
4474            .map(function(location) {
4475                return location.getLabel();
4476            })
4477            .join(", ");
4478    }
4479};
4480
4481jqapi.extend(Constraint.prototype, {
4482    isValid: function() {
4483        return this.locations.length > 0;
4484    },
4485    getFields: function() {
4486        return this.locations.map(function(location) {
4487            return location.getFields();
4488        });
4489    }
4490});
4491
4492var methods$5 = {
4493    createConstraints: function() {
4494        var that = this;
4495        that.constraints = {};
4496    },
4497    setupConstraints: function() {
4498        var that = this,
4499            constraints = that.options.constraints,
4500            $parent;
4501
4502        if (!constraints) {
4503            that.unbindFromParent();
4504            return;
4505        }
4506
4507        if (
4508            jqapi.isJqObject(constraints) ||
4509            typeof constraints === "string" ||
4510            typeof constraints.nodeType === "number"
4511        ) {
4512            $parent = jqapi.select(constraints);
4513            if (!$parent.is(that.constraints)) {
4514                that.unbindFromParent();
4515                if (!$parent.is(that.el)) {
4516                    that.constraints = $parent;
4517                    that.bindToParent();
4518                }
4519            }
4520        } else {
4521            collection_util.each(that.constraints, function(_, id) {
4522                that.removeConstraint(id);
4523            });
4524            collection_util.each(
4525                collection_util.makeArray(constraints),
4526                function(constraint, i) {
4527                    that.addConstraint(constraint);
4528                }
4529            );
4530        }
4531    },
4532
4533    filteredLocation: function(data) {
4534        var locationComponents = [],
4535            location = {};
4536
4537        collection_util.each(this.type.dataComponents, function() {
4538            if (this.forLocations) locationComponents.push(this.id);
4539        });
4540
4541        if (lang_util.isPlainObject(data)) {
4542            // Copy to location only allowed fields
4543            collection_util.each(data, function(value, key) {
4544                if (value && locationComponents.indexOf(key) >= 0) {
4545                    location[key] = value;
4546                }
4547            });
4548        }
4549
4550        if (!lang_util.isEmptyObject(location)) {
4551            return location.kladr_id
4552                ? { kladr_id: location.kladr_id }
4553                : location;
4554        }
4555    },
4556
4557    addConstraint: function(constraint) {
4558        var that = this;
4559
4560        constraint = new Constraint(constraint, that);
4561
4562        if (constraint.isValid()) {
4563            that.constraints[constraint.id] = constraint;
4564        }
4565    },
4566
4567    removeConstraint: function(id) {
4568        var that = this;
4569        delete that.constraints[id];
4570    },
4571
4572    constructConstraintsParams: function() {
4573        var that = this,
4574            locations = [],
4575            constraints = that.constraints,
4576            parentInstance,
4577            parentData,
4578            params = {};
4579
4580        while (
4581            jqapi.isJqObject(constraints) &&
4582            (parentInstance = constraints.suggestions()) &&
4583            !(parentData = object_util.getDeepValue(
4584                parentInstance,
4585                "selection.data"
4586            ))
4587        ) {
4588            constraints = parentInstance.constraints;
4589        }
4590
4591        if (jqapi.isJqObject(constraints)) {
4592            parentData = new ConstraintLocation(
4593                parentData,
4594                parentInstance
4595            ).getFields();
4596
4597            if (parentData) {
4598                // if send city_fias_id for city request
4599                // then no cities will responded
4600                if (that.bounds.own.indexOf("city") > -1) {
4601                    delete parentData.city_fias_id;
4602                }
4603                params.locations = [parentData];
4604                params.restrict_value = true;
4605            }
4606        } else {
4607            if (constraints) {
4608                collection_util.each(constraints, function(constraint, id) {
4609                    locations = locations.concat(constraint.getFields());
4610                });
4611
4612                if (locations.length) {
4613                    params.locations = locations;
4614                    params.restrict_value = that.options.restrict_value;
4615                }
4616            }
4617        }
4618
4619        return params;
4620    },
4621
4622    /**
4623     * Returns label of the first constraint (if any), empty string otherwise
4624     * @returns {String}
4625     */
4626    getFirstConstraintLabel: function() {
4627        var that = this,
4628            constraints_id =
4629                lang_util.isPlainObject(that.constraints) &&
4630                Object.keys(that.constraints)[0];
4631
4632        return constraints_id ? that.constraints[constraints_id].label : "";
4633    },
4634
4635    bindToParent: function() {
4636        var that = this;
4637
4638        that.constraints
4639            .on(
4640                [
4641                    "suggestions-select." + that.uniqueId,
4642                    "suggestions-invalidateselection." + that.uniqueId,
4643                    "suggestions-clear." + that.uniqueId
4644                ].join(" "),
4645                jqapi.proxy(that.onParentSelectionChanged, that)
4646            )
4647            .on(
4648                "suggestions-dispose." + that.uniqueId,
4649                jqapi.proxy(that.onParentDispose, that)
4650            );
4651    },
4652
4653    unbindFromParent: function() {
4654        var that = this,
4655            $parent = that.constraints;
4656
4657        if (jqapi.isJqObject($parent)) {
4658            $parent.off("." + that.uniqueId);
4659        }
4660    },
4661
4662    onParentSelectionChanged: function(e, suggestion, valueChanged) {
4663        // Don't clear if parent has been just enriched
4664        if (e.type !== "suggestions-select" || valueChanged) {
4665            this.clear();
4666        }
4667    },
4668
4669    onParentDispose: function(e) {
4670        this.unbindFromParent();
4671    },
4672
4673    getParentInstance: function() {
4674        return (
4675            jqapi.isJqObject(this.constraints) && this.constraints.suggestions()
4676        );
4677    },
4678
4679    shareWithParent: function(suggestion) {
4680        // that is the parent control's instance
4681        var that = this.getParentInstance();
4682
4683        if (
4684            !that ||
4685            that.type !== this.type ||
4686            belongsToArea(suggestion, that)
4687        ) {
4688            return;
4689        }
4690
4691        that.shareWithParent(suggestion);
4692        that.setSuggestion(suggestion);
4693    },
4694
4695    /**
4696     * Pick only fields that absent in restriction
4697     */
4698    getUnrestrictedData: function(data) {
4699        var that = this,
4700            restrictedKeys = [],
4701            unrestrictedData = {},
4702            maxSpecificity = -1;
4703
4704        // Find most specific location that could restrict current data
4705        collection_util.each(that.constraints, function(constraint, id) {
4706            collection_util.each(constraint.locations, function(location, i) {
4707                if (
4708                    location.containsData(data) &&
4709                    location.specificity > maxSpecificity
4710                ) {
4711                    maxSpecificity = location.specificity;
4712                }
4713            });
4714        });
4715
4716        if (maxSpecificity >= 0) {
4717            // Для городов-регионов нужно также отсечь и город
4718            if (
4719                data.region_kladr_id &&
4720                data.region_kladr_id === data.city_kladr_id
4721            ) {
4722                restrictedKeys.push.apply(
4723                    restrictedKeys,
4724                    that.type.dataComponentsById["city"].fields
4725                );
4726            }
4727
4728            // Collect all fieldnames from all restricted components
4729            collection_util.each(
4730                that.type.dataComponents.slice(0, maxSpecificity + 1),
4731                function(component, i) {
4732                    restrictedKeys.push.apply(restrictedKeys, component.fields);
4733                }
4734            );
4735
4736            // Copy skipping restricted fields
4737            collection_util.each(data, function(value, key) {
4738                if (restrictedKeys.indexOf(key) === -1) {
4739                    unrestrictedData[key] = value;
4740                }
4741            });
4742        } else {
4743            unrestrictedData = data;
4744        }
4745
4746        return unrestrictedData;
4747    }
4748};
4749
4750jqapi.extend(DEFAULT_OPTIONS, optionsUsed$1);
4751
4752jqapi.extend(Suggestions.prototype, methods$5);
4753
4754// Disable this feature when GET method used. See SUG-202
4755if (ajax.getDefaultType() != "GET") {
4756    notificator
4757        .on("initialize", methods$5.createConstraints)
4758        .on("setOptions", methods$5.setupConstraints)
4759        .on("requestParams", methods$5.constructConstraintsParams)
4760        .on("dispose", methods$5.unbindFromParent);
4761}
4762
4763/**
4764 * Methods for selecting a suggestion
4765 */
4766
4767var methods$6 = {
4768    proceedQuery: function(query) {
4769        var that = this;
4770
4771        if (query.length >= that.options.minChars) {
4772            that.updateSuggestions(query);
4773        } else {
4774            that.hide();
4775        }
4776    },
4777
4778    /**
4779     * Selects current or first matched suggestion, but firstly waits for data ready
4780     * @param selectionOptions
4781     * @returns {$.Deferred} promise, resolved with index of selected suggestion or rejected if nothing matched
4782     */
4783    selectCurrentValue: function(selectionOptions) {
4784        var that = this,
4785            result = jqapi.Deferred();
4786
4787        // force onValueChange to be executed if it has been deferred
4788        that.inputPhase.resolve();
4789
4790        that.fetchPhase
4791            .done(function() {
4792                var index;
4793
4794                // When suggestion has already been selected and not modified
4795                if (that.selection && !that.visible) {
4796                    result.reject();
4797                } else {
4798                    index = that.findSuggestionIndex();
4799
4800                    that.select(index, selectionOptions);
4801
4802                    if (index === -1) {
4803                        result.reject();
4804                    } else {
4805                        result.resolve(index);
4806                    }
4807                }
4808            })
4809            .fail(function() {
4810                result.reject();
4811            });
4812
4813        return result;
4814    },
4815
4816    /**
4817     * Selects first when user interaction is not supposed
4818     */
4819    selectFoundSuggestion: function() {
4820        var that = this;
4821
4822        if (!that.requestMode.userSelect) {
4823            that.select(0);
4824        }
4825    },
4826
4827    /**
4828     * Selects current or first matched suggestion
4829     * @returns {number} index of found suggestion
4830     */
4831    findSuggestionIndex: function() {
4832        var that = this,
4833            index = that.selectedIndex,
4834            value;
4835
4836        if (index === -1) {
4837            // matchers always operate with trimmed strings
4838            value = that.el.val().trim();
4839            if (value) {
4840                that.type.matchers.some(function(matcher) {
4841                    index = matcher(value, that.suggestions);
4842                    return index !== -1;
4843                });
4844            }
4845        }
4846
4847        return index;
4848    },
4849
4850    /**
4851     * Selects a suggestion at specified index
4852     * @param index index of suggestion to select. Can be -1
4853     * @param {Object} selectionOptions
4854     * @param {boolean} [selectionOptions.continueSelecting]  prevents hiding after selection
4855     * @param {boolean} [selectionOptions.noSpace]  prevents adding space at the end of current value
4856     */
4857    select: function(index, selectionOptions) {
4858        var that = this,
4859            suggestion = that.suggestions[index],
4860            continueSelecting =
4861                selectionOptions && selectionOptions.continueSelecting,
4862            currentValue = that.currentValue,
4863            hasSameValues;
4864
4865        // Prevent recursive execution
4866        if (that.triggering["Select"]) return;
4867
4868        // if no suggestion to select
4869        if (!suggestion) {
4870            if (!continueSelecting && !that.selection) {
4871                that.triggerOnSelectNothing();
4872            }
4873            that.onSelectComplete(continueSelecting);
4874            return;
4875        }
4876
4877        hasSameValues = that.hasSameValues(suggestion);
4878
4879        that.enrichSuggestion(suggestion, selectionOptions).done(function(
4880            enrichedSuggestion,
4881            hasBeenEnriched
4882        ) {
4883            var newSelectionOptions = jqapi.extend(
4884                {
4885                    hasBeenEnriched: hasBeenEnriched,
4886                    hasSameValues: hasSameValues
4887                },
4888                selectionOptions
4889            );
4890            that.selectSuggestion(
4891                enrichedSuggestion,
4892                index,
4893                currentValue,
4894                newSelectionOptions
4895            );
4896        });
4897    },
4898
4899    /**
4900     * Formats and selects final (enriched) suggestion
4901     * @param suggestion
4902     * @param index
4903     * @param lastValue
4904     * @param {Object} selectionOptions
4905     * @param {boolean} [selectionOptions.continueSelecting]  prevents hiding after selection
4906     * @param {boolean} [selectionOptions.noSpace]  prevents adding space at the end of current value
4907     * @param {boolean} selectionOptions.hasBeenEnriched
4908     * @param {boolean} selectionOptions.hasSameValues
4909     */
4910    selectSuggestion: function(suggestion, index, lastValue, selectionOptions) {
4911        var that = this,
4912            continueSelecting = selectionOptions.continueSelecting,
4913            assumeDataComplete =
4914                !that.type.isDataComplete ||
4915                that.type.isDataComplete.call(that, suggestion),
4916            currentSelection = that.selection;
4917
4918        // Prevent recursive execution
4919        if (that.triggering["Select"]) return;
4920
4921        if (that.type.alwaysContinueSelecting) {
4922            continueSelecting = true;
4923        }
4924
4925        if (assumeDataComplete) {
4926            continueSelecting = false;
4927        }
4928
4929        // `suggestions` cat be empty, e.g. during `fixData`
4930        if (selectionOptions.hasBeenEnriched && that.suggestions[index]) {
4931            that.suggestions[index].data = suggestion.data;
4932        }
4933
4934        if (that.requestMode.updateValue) {
4935            that.checkValueBounds(suggestion);
4936            that.currentValue = that.getSuggestionValue(
4937                suggestion,
4938                selectionOptions
4939            );
4940
4941            if (
4942                that.currentValue &&
4943                !selectionOptions.noSpace &&
4944                !assumeDataComplete
4945            ) {
4946                that.currentValue += " ";
4947            }
4948            that.el.val(that.currentValue);
4949        }
4950
4951        if (that.currentValue) {
4952            that.selection = suggestion;
4953            if (!that.areSuggestionsSame(suggestion, currentSelection)) {
4954                that.trigger(
4955                    "Select",
4956                    suggestion,
4957                    that.currentValue != lastValue
4958                );
4959            }
4960            if (that.requestMode.userSelect) {
4961                that.onSelectComplete(continueSelecting);
4962            }
4963        } else {
4964            that.selection = null;
4965            that.triggerOnSelectNothing();
4966        }
4967
4968        that.shareWithParent(suggestion);
4969    },
4970
4971    onSelectComplete: function(continueSelecting) {
4972        var that = this;
4973
4974        if (continueSelecting) {
4975            that.selectedIndex = -1;
4976            that.updateSuggestions(that.currentValue);
4977        } else {
4978            that.hide();
4979        }
4980    },
4981
4982    triggerOnSelectNothing: function() {
4983        var that = this;
4984
4985        if (!that.triggering["SelectNothing"]) {
4986            that.trigger("SelectNothing", that.currentValue);
4987        }
4988    },
4989
4990    trigger: function(event) {
4991        var that = this,
4992            args = utils.slice(arguments, 1),
4993            callback = that.options["on" + event];
4994
4995        that.triggering[event] = true;
4996        if (utils.isFunction(callback)) {
4997            callback.apply(that.element, args);
4998        }
4999        that.el.trigger.call(
5000            that.el,
5001            "suggestions-" + event.toLowerCase(),
5002            args
5003        );
5004        that.triggering[event] = false;
5005    }
5006};
5007
5008jqapi.extend(Suggestions.prototype, methods$6);
5009
5010notificator.on("assignSuggestions", methods$6.selectFoundSuggestion);
5011
5012/**
5013 * features for connected instances
5014 */
5015
5016var optionsUsed$2 = {
5017    bounds: null,
5018};
5019
5020var methods$7 = {
5021    setupBounds: function() {
5022        this.bounds = {
5023            from: null,
5024            to: null,
5025        };
5026    },
5027
5028    setBoundsOptions: function() {
5029        var that = this,
5030            boundsAvailable = [],
5031            newBounds = $.trim(that.options.bounds).split("-"),
5032            boundFrom = newBounds[0],
5033            boundTo = newBounds[newBounds.length - 1],
5034            boundsOwn = [],
5035            boundIsOwn,
5036            boundsAll = [];
5037
5038        if (that.type.dataComponents) {
5039            $.each(that.type.dataComponents, function() {
5040                if (this.forBounds) {
5041                    boundsAvailable.push(this.id);
5042                }
5043            });
5044        }
5045
5046        if (boundsAvailable.indexOf(boundFrom) === -1) {
5047            boundFrom = null;
5048        }
5049
5050        if (boundsAvailable.indexOf(boundTo) === -1) {
5051            boundTo = null;
5052        }
5053
5054        if (boundFrom || boundTo) {
5055            boundIsOwn = !boundFrom;
5056            $.each(boundsAvailable, function(i, bound) {
5057                if (bound == boundFrom) {
5058                    boundIsOwn = true;
5059                }
5060                boundsAll.push(bound);
5061                if (boundIsOwn) {
5062                    boundsOwn.push(bound);
5063                }
5064                if (bound == boundTo) {
5065                    return false;
5066                }
5067            });
5068        }
5069
5070        that.bounds.from = boundFrom;
5071        that.bounds.to = boundTo;
5072        that.bounds.all = boundsAll;
5073        that.bounds.own = boundsOwn;
5074    },
5075
5076    constructBoundsParams: function() {
5077        var that = this,
5078            params = {};
5079
5080        if (that.bounds.from) {
5081            params["from_bound"] = { value: that.bounds.from };
5082        }
5083        if (that.bounds.to) {
5084            params["to_bound"] = { value: that.bounds.to };
5085        }
5086
5087        return params;
5088    },
5089
5090    /**
5091     * Подстраивает suggestion.value под that.bounds.own
5092     * Ничего не возвращает, меняет в самом suggestion
5093     * @param suggestion
5094     */
5095    checkValueBounds: function(suggestion) {
5096        var that = this,
5097            valueData;
5098
5099        // If any bounds set up
5100        if (that.bounds.own.length && that.type.composeValue) {
5101            // делаем копию
5102            var bounds = that.bounds.own.slice(0);
5103            // если роль текущего инстанса плагина показывать только район города
5104            // то для корректного формировния нужен city_district_fias_id
5105            if (bounds.length === 1 && bounds[0] === "city_district") {
5106                bounds.push("city_district_fias_id");
5107            }
5108            valueData = that.copyDataComponents(suggestion.data, bounds);
5109            suggestion.value = that.type.composeValue(valueData);
5110        }
5111    },
5112
5113    copyDataComponents: function(data, components) {
5114        var result = {},
5115            dataComponentsById = this.type.dataComponentsById;
5116
5117        if (dataComponentsById) {
5118            $.each(components, function(i, component) {
5119                $.each(dataComponentsById[component].fields, function(
5120                    i,
5121                    field
5122                ) {
5123                    if (data[field] != null) {
5124                        result[field] = data[field];
5125                    }
5126                });
5127            });
5128        }
5129
5130        return result;
5131    },
5132
5133    getBoundedKladrId: function(kladr_id, boundsRange) {
5134        var boundTo = boundsRange[boundsRange.length - 1],
5135            kladrFormat;
5136
5137        $.each(this.type.dataComponents, function(i, component) {
5138            if (component.id === boundTo) {
5139                kladrFormat = component.kladrFormat;
5140                return false;
5141            }
5142        });
5143
5144        return (
5145            kladr_id.substr(0, kladrFormat.digits) +
5146            new Array((kladrFormat.zeros || 0) + 1).join("0")
5147        );
5148    },
5149};
5150
5151$.extend(DEFAULT_OPTIONS, optionsUsed$2);
5152
5153$.extend(Suggestions.prototype, methods$7);
5154
5155notificator
5156    .on("initialize", methods$7.setupBounds)
5157    .on("setOptions", methods$7.setBoundsOptions)
5158    .on("requestParams", methods$7.constructBoundsParams);
5159
5160/**
5161 * Утилиты для работы с DOM.
5162 */
5163var dom = {
5164    /**
5165     * Выбрать первый элемент с указанным классом.
5166     */
5167    selectByClass: function(classname, parent) {
5168        var selector = "." + classname;
5169        if (parent) {
5170            return parent.querySelector(selector);
5171        } else {
5172            return document.querySelector(selector);
5173        }
5174    },
5175
5176    /**
5177     * Добавить элементу класс.
5178     */
5179    addClass: function(element, className) {
5180        var list = element.className.split(" ");
5181        if (list.indexOf(className) === -1) {
5182            list.push(className);
5183        }
5184        element.className = list.join(" ");
5185    },
5186
5187    /**
5188     * Добавить элементу стиль.
5189     */
5190    setStyle: function(element, name, value) {
5191        element.style[name] = value;
5192    },
5193
5194    /**
5195     * Подписаться на событие на элементе.
5196     * @param {Element} element - элемент
5197     * @param {string} eventName - название события
5198     * @param {string} namespace - пространство имён события
5199     * @param {Function} callback - функция-обработчик события
5200     */
5201    listenTo: function(element, eventName, namespace, callback) {
5202        element.addEventListener(eventName, callback, false);
5203        if (namespace) {
5204            if (!eventsByNamespace[namespace]) {
5205                eventsByNamespace[namespace] = [];
5206            }
5207            eventsByNamespace[namespace].push({
5208                eventName: eventName,
5209                element: element,
5210                callback: callback
5211            });
5212        }
5213    },
5214
5215    /**
5216     * Отписаться от всех событий с указанным пространством имён.
5217     */
5218    stopListeningNamespace: function(namespace) {
5219        var events = eventsByNamespace[namespace];
5220        if (events) {
5221            events.forEach(function(event) {
5222                event.element.removeEventListener(
5223                    event.eventName,
5224                    event.callback,
5225                    false
5226                );
5227            });
5228        }
5229    }
5230};
5231
5232/**
5233 * Промо-ссылка в списке подсказок.
5234 */
5235var FREE_PLAN = "FRE
5235 
vendor: 4,686 bytes, lines 5235-5314
5235E";
5236var LINK =
5237    "https://dadata.ru/suggestions/?utm_source=dadata&utm_medium=module&utm_campaign=suggestions-jquery";
5238var PREFIX = "";
5239var SUFFIX = "";
5240var IMAGE =
5241    '<svg version="1.1" viewBox="0 0 128 38" xmlns="http://www.w3.org/2000/svg"><path d="m128 19v16.077c0 1.614-1.302 2.923-2.909 2.923h-122.18c-1.607 0-2.909-1.309-2.909-2.923v-32.154c-0-1.614 1.302-2.923 2.909-2.923h122.18c1.607 0 2.909 1.309 2.909 2.923z" fill="#ef4741"/><path d="m59.52 7.912h-8.902v22.098h9.92c3.724 0 9.872-0.341 9.872-6.703v-8.682c-0.01-6.372-7.166-6.713-10.89-6.713zm5.595 14.81c0 3.186-2.308 3.508-4.936 3.508h-4.276v-14.538h3.287c2.628 0 5.954 0.322 5.954 3.508zm-46.545-14.81h-8.834v22.098h9.871c3.724 0 9.872-0.341 9.872-6.703v-8.682c0-6.372-7.137-6.713-10.88-6.713zm5.595 14.81c0 3.186-2.308 3.508-4.936 3.508h-4.247v-14.538h3.258c2.628 0 5.954 0.322 5.954 3.508zm71.757-13.953h-4.945v16.301c-0.018 0.785 0.113 1.565 0.388 2.3 0.203 0.569 0.535 1.082 0.97 1.5 0.446 0.385 0.962 0.677 1.522 0.858 0.58 0.205 1.182 0.343 1.794 0.409 0.575 0.052 1.248 0.081 2.017 0.088 0.917-1e-3 1.834-0.057 2.744-0.166v-2.796h-1.765c-0.393 0.055-0.795 0.032-1.18-0.071-0.385-0.102-0.745-0.28-1.06-0.524-0.413-0.691-0.59-1.498-0.504-2.299v-8.068h4.509v-3.06h-4.509zm20.364 5.535c-1.176-0.741-3.278-1.108-6.303-1.101h-5.741v0.243l0.708 2.826h5.033c0.837-0.051 1.672 0.117 2.424 0.487 0.259 0.248 0.458 0.553 0.579 0.891 0.121 0.339 0.162 0.701 0.119 1.058v1.12h-5.527c-1.939 0-3.271 0.38-3.995 1.14-0.725 0.76-1.099 2.127-1.125 4.102 0 2.154 0.359 3.06 1.086 3.742 0.728 0.682 2.134 1.188 4.344 1.188h6.847c1.706 0 3.345-0.808 3.345-2.747v-8.584c0-2.176-0.589-3.635-1.765-4.375zm-3.19 12.959h-3.249c-0.735 0.081-1.478-0.036-2.152-0.342-0.285-0.227-0.427-0.876-0.427-1.948s0.136-1.741 0.407-2.007c0.625-0.331 1.336-0.46 2.037-0.371h3.384zm-26.667-12.959c-1.176-0.741-3.277-1.108-6.303-1.101h-5.741v0.243l0.708 2.826h5.033c0.836-0.051 1.672 0.117 2.424 0.487 0.259 0.248 0.457 0.553 0.578 0.891 0.121 0.339 0.162 0.701 0.12 1.058v1.12h-5.556c-1.939 0-3.271 0.38-3.995 1.14s-1.086 2.127-1.086 4.102c0 2.154 0.359 3.06 1.086 3.742s2.133 1.188 4.344 1.188h6.846c1.717 0 3.346-0.808 3.346-2.747v-8.584c-7e-3 -2.176-0.595-3.635-1.765-4.375zm-3.181 12.959h-3.248c-0.735 0.081-1.478-0.037-2.153-0.342-0.284-0.227-0.426-0.876-0.426-1.948s0.135-1.741 0.407-2.007c0.624-0.331 1.336-0.46 2.036-0.371h3.384zm-37.74-12.959c-1.176-0.741-3.278-1.108-6.303-1.101h-5.712v0.243l0.708 2.826h5.033c0.837-0.051 1.672 0.117 2.424 0.487 0.259 0.248 0.457 0.553 0.578 0.891 0.121 0.339 0.162 0.701 0.12 1.058v1.12h-5.556c-1.939 0-3.271 0.38-3.995 1.14s-1.099 2.127-1.125 4.102c0 2.154 0.359 3.06 1.086 3.742s2.133 1.188 4.344 1.188h6.846c1.717 0 3.346-0.808 3.346-2.747v-8.584c0-2.176-0.589-3.635-1.765-4.375zm-3.181 12.959h-3.219c-0.735 0.081-1.478-0.037-2.153-0.342-0.284-0.227-0.427-0.876-0.427-1.948s0.136-1.741 0.408-2.007c0.624-0.331 1.336-0.46 2.036-0.371h3.384z" fill="#fff"/></svg>';
5242
5243function Promo(plugin) {
5244    this.plan = plugin.status.plan;
5245    var container = plugin.getContainer();
5246    this.element = dom.selectByClass(CLASSES.promo, container);
5247}
5248
5249Promo.prototype.show = function() {
5250    if (this.plan !== FREE_PLAN) {
5251        return;
5252    }
5253    if (!this.element) {
5254        return;
5255    }
5256    this.setStyles();
5257    this.setHtml();
5258};
5259
5260Promo.prototype.setStyles = function() {
5261    this.element.style.display = "block";
5262};
5263
5264Promo.prototype.setHtml = function() {
5265    this.element.innerHTML =
5266        '<a target="_blank" tabindex="-1" href="' +
5267        LINK +
5268        '">' +
5269        PREFIX +
5270        IMAGE +
5271        SUFFIX +
5272        "</a>";
5273};
5274
5275function show() {
5276    new Promo(this).show();
5277}
5278
5279notificator.on("assignSuggestions", show);
5280
5281Suggestions.defaultOptions = DEFAULT_OPTIONS;
5282
5283Suggestions.version = "22.6.0";
5284
5285$.Suggestions = Suggestions;
5286
5287// Create chainable jQuery plugin:
5288$.fn.suggestions = function(options, args) {
5289    // If function invoked without argument return
5290    // instance of the first matched element:
5291    if (arguments.length === 0) {
5292        return this.first().data(DATA_ATTR_KEY);
5293    }
5294
5295    return this.each(function() {
5296        var inputElement = $(this),
5297            instance = inputElement.data(DATA_ATTR_KEY);
5298
5299        if (typeof options === "string") {
5300            if (instance && typeof instance[options] === "function") {
5301                instance[options](args);
5302            }
5303        } else {
5304            // If instance already exists, destroy it:
5305            if (instance && instance.dispose) {
5306                instance.dispose();
5307            }
5308            instance = new Suggestions(this, options);
5309            inputElement.data(DATA_ATTR_KEY, instance);
5310        }
5311    });
5312};
5313
5314})));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.