WaniKani Open Framework Kanjidic2 and Traditional Radicals Filters

Kanjidic2 and traditional radicals filters for the WaniKani Open Framework

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name          WaniKani Open Framework Kanjidic2 and Traditional Radicals Filters
// @namespace     https://www.wanikani.com
// @description   Kanjidic2 and traditional radicals filters for the WaniKani Open Framework
// @author        prouleau
// @version       1.4.2
// @match         https://www.wanikani.com/*
// @license       GPLV3; https://www.gnu.org/licenses/gpl-3.0.en.html and MIT; http://opensource.org/licenses/MIT --- with an exception described in comments
// @grant         none
// ==/UserScript==


// ===============================================================
// Software Licence
//
// The license is GPLV3 or later because this script includes @acm2010 code and databases licenced under GPLV3 or later
// --- for Keisei Semantic-Phonetic Composition
// --- for Niai Visual Similarity Data
//
// Code borrowed from WKOF is licensed under MIT --- http://opensource.org/licenses/MIT
//
// You may use Item Inspector code under either the GPLV3 or MIT license with these restrictions.
// --- The GPLV3 or later code and database borrowed from @acm2010 must remain licensed under GPLV3 or later in all cases.
// --- If you use @acm2010 code and/or databases you work as a whole must be licensed under GPLV3 or later to comply with @acm2010 license.
// --- The MIT code borrowed from WKOF must remain licensed under MIT in all cases.
// These restrictions are required because we can't legally change the license for someone else's code and database without their permission.
// Not even if we modify their code.
//
// ===============================================================

/* globals $, _, LZMA  */
/* eslint-disable no-eval */
/* eslint-disable no-multi-spaces */
/* eslint-disable curly */
/* eslint-disable no-return-assign */

var advSearchFilters = {};

(function(wkof) {
    'use strict';

    //=================================================================
    // These filters are button type filters
    // They need a path to be defined in the config parameter of the on_click handler to locate where to store the filter parameters.
    // The exception is Self Study Quiz because the path is automatically figured out upon detection that the on_click function is called from Self Study Quiz
    //
    // They support the callable dialog protocol, meaning that the on_click handler is callable directly without having to click an actual button
    // The support of this protocol is indicated by the flag  callable_dialog: true  in the registry entry for the filter
    //
    // Callable on_click handlers support in their config parameter three callbacks and require one parameter
    //
    // on_save:   called when the settings are saved
    // on_cancel: called when the settings are cancelled
    // on_close:  called when the dialog is closed
    // script_id: (required when called without clicking a button) must be the script id used to store wkof settings. (wkof.settings[script_id])
    //
    //=================================================================

    var wkofMinimumVersion = '1.0.52';

    if (!wkof) {
        var response = confirm('WaniKani Open Framework Date Filters requires WaniKani Open Framework.\n Click "OK" to be forwarded to installation instructions.');

        if (response) {
            window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549';
        }

        return;
    }

    // --------------------------------------
    // File names for resources to be loaded.

    // Prerequisite scripts
    const lodash_file = 'https://raw.githubusercontent.com/rouleaup88/item-inspector/main/lodash.min.js';
    const lzma_file = 'https://raw.githubusercontent.com/rouleaup88/item-inspector/main/lzma.js';
    const lzma_shim_file = 'https://raw.githubusercontent.com/rouleaup88/item-inspector/main/lzma.shim.js';

    // Stroke count for Wanikani radicals
    const Wkit_StrokeCountforRadicals = 'https://raw.githubusercontent.com/rouleaup88/item-inspector/main/WK_radicals.json.compressed';

    // Traditional radicals items
    const traditionaRadicalsFile = 'https://raw.githubusercontent.com/rouleaup88/item-inspector/main/trad_rad.json.compressed';

    // Kanjidic 2 data
    const kanjidic2File = 'https://raw.githubusercontent.com/rouleaup88/item-inspector/main/kanjidic2.json.compressed';

    // Keisei Semantic-Phonetic Composition databases
    const kanji_db = 'https://raw.githubusercontent.com/rouleaup88/item-inspector/main/kanji_esc.json.compressed';
    const phonetic_db = 'https://raw.githubusercontent.com/rouleaup88/item-inspector/main/phonetic_esc.json.compressed';
    const wk_kanji_db = 'https://raw.githubusercontent.com/rouleaup88/item-inspector/main/wk_kanji_esc.json.compressed';

    // Lars Yencken visually similar kanji database - also required for Niai visual similarity
    const visuallySimilarFilename = 'https://raw.githubusercontent.com/rouleaup88/item-inspector/main/stroke_edit_dist_esc.json.compressed';

    // Niai Visually similar databases
    const from_keisei_db_filename ='https://raw.githubusercontent.com/rouleaup88/item-inspector/main/from_keisei_esc.json.compressed';
    const old_script_db_filename ='https://raw.githubusercontent.com/rouleaup88/item-inspector/main/old_script_esc.json.compressed';
    const wk_niai_noto_db_filename ='https://raw.githubusercontent.com/rouleaup88/item-inspector/main/wk_niai_noto_esc.json.compressed';
    const yl_radical_db_filename ='https://raw.githubusercontent.com/rouleaup88/item-inspector/main/yl_radical_esc.json.compressed';

    // END of filenames
    //-------------------

    var settingsScriptId = 'advSearchFilters';
    var settingsTitle = 'Advanced Search Filters';

    var needToRegisterFilters = true;

    var filterNamePrefix = 'advSearchFilters_';
    var advancedSearchFilterName = filterNamePrefix + 'advancedSearch';
    var relatedSearchFilterName = filterNamePrefix + 'relatedSearch';
    var extensiveSearchFilterName = filterNamePrefix + 'extensiveSearch';
    var strokeCountGteqFilterName = filterNamePrefix + 'strokeCountGteq';
    var strokeCountLteqFilterName = filterNamePrefix + 'strokeCountLteq';
    var explicitListFilterName = filterNamePrefix + 'explicitList';
    var explicitBlockFilterName = filterNamePrefix + 'explicitBlock';

    var supportedFilters = [advancedSearchFilterName, relatedSearchFilterName, extensiveSearchFilterName, strokeCountGteqFilterName, strokeCountLteqFilterName,
                            explicitListFilterName, explicitBlockFilterName, ];

    function startupSequence(){
        wkof.set_state(settingsScriptId, 'loading');
        var config = {wk_items: {filters:{}, options:{'subjects': true,}}};

        wkof.include('ItemData, Settings');
        loadPrerequisiteScripts()
            .then(function(){return wkof.ready('ItemData, Settings')})
            .then(function(){return wkof.Settings.load(settingsScriptId)})
            .then(ageCache)
            .then(updateFiltersWhenReady)
    };

    var componentIndexKan = {};
    var componentIndexVoc = {};
    var usedInIndexRad = {};
    var usedInIndexKan = {};
    var WKvisuallySimilarIndex = {};
    function prepareIndex(items){
        for (var item of items){
            if (item.object === 'kanji'){
                componentIndexKan[item.data.slug] = item.data.component_subject_ids;
                usedInIndexKan[item.data.slug] = item.data.amalgamation_subject_ids;
                WKvisuallySimilarIndex[item.data.slug] = item.data.visually_similar_subject_ids;
            };
            if (item.object === 'radical'){
                usedInIndexRad[item.data.slug] = item.data.amalgamation_subject_ids;
                if (item.data.characters !== null) usedInIndexRad[item.data.characters] = item.data.amalgamation_subject_ids;
            };
            if (item.object !== 'vocabulary'){
                componentIndexVoc[item.data.slug] = item.data.amalgamation_subject_ids;
            };
        };
    };

    function ageCache(){
        // periodically ages the cache
        let ageingTime = 1000*60*60*24*30*2;  // two months
        var now = Date.now();
        if (wkof.settings[settingsScriptId] === undefined) wkof.settings[settingsScriptId] = {};
        if (wkof.settings[settingsScriptId].lastTime === undefined){
            wkof.settings[settingsScriptId].lastTime = now;
            wkof.Settings.save(settingsScriptId);
        }
        let lastTime = wkof.settings[settingsScriptId].lastTime;
        if (now > lastTime + ageingTime){
            deleteCache();
            wkof.settings[settingsScriptId].lastTime = now;
            wkof.Settings.save(settingsScriptId);
        };
    };

    function deleteCache(){
        deleteKeiseiCache();
        deleteVisuallySimilarCache();
        deleteNiaiCache();
        kanjidic2_cacheDelete();
        WkStrokeCount_cacheDelete()
        trad_rad_cacheDelete();
        wkof.file_cache.delete(lodash_file);
        wkof.file_cache.delete(lzma_file);
        wkof.file_cache.delete(lzma_shim_file);
    };

    function updateFiltersWhenReady() {
        // register the filters
        needToRegisterFilters = true;
        return waitForItemDataRegistry().then(registerFilters);
    };

    function waitForItemDataRegistry() {
        return wkof.wait_state('wkof.ItemData.registry', 'ready');
    };

    function registerFilters() {
        if (!needToRegisterFilters) {
            return;
        };

        supportedFilters.forEach(function(filterName) {
            delete wkof.ItemData.registry.sources.wk_items.filters[filterName];
        });

        registerTraditionalRadicals(); // must be first
        registerAdvancedSearchFilter();
        registerRelatedSearchFilter();
        registerExtensiveSearchFilter();
        registerStrokeCountGteqilter();
        registerStrokeCountLteqilter();
        registerExplicitListFilter();
        registerExplicitBlockFilter();

        needToRegisterFilters = false;
        wkof.set_state(settingsScriptId, 'ready');
    };

    // Unicode for Japanese characters
    // reference https://stackoverflow.com/questions/15033196/using-javascript-to-check-whether-a-string-contains-japanese-characters-includi
    //
    // regexes for splitting stings into words, one to get composed words that may have ' and - and one for subwords separated by ' or -
    const breakTheWords = /[^\-'a-zA-Z0-9\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf\u3400-\u4dbf]/;
    const breakTheSubwords = /[\-']/;
    const noHtml = /<[^>]*>/g;

    // constants for searches

    const all = '*';
    const none = '!*'

    // ------------------
    // helping functions

    // split_list() is borrowed from @rfindley -- must be licensed under MIT
    function split_list(str) {return str.replace(/、/g,',')
        .replace(/[\s ]+/g,' ')
        .replace(/!/g, '!')
        .replace(/*/g, '*')
        .trim()
        .replace(/ *, */g, ',')
        .replace(/ *, */g, ',')
        .toLowerCase()
        .split(',')
        .filter(function(name) {return (name.length > 0);});
     };

    // detailed path parsing to make sure it is a real path and not something else -- much needed to keep eval safe to use
    // there is no alternative to eval in this context
    // function set_value() and get_value is borrowed from wkof Settings module -- must be under MIT license
    function set_value(path, value) {
        var depth=0, new_path='', param, c;
        for (var idx = 0; idx < path.length; idx++) {
            c = path[idx];
            if (c === '[') {
                if (depth++ === 0) {
                    new_path += '[';
                    param = '';
                } else {
                    param += '[';
                }
            } else if (c === ']') {
                if (--depth === 0) {
                    new_path += JSON.stringify(eval(param)) + ']';
                } else {
                    param += ']';
                }
            } else {
                if (c === '@') c = 'base.';
                if (depth === 0)
                    new_path += c;
                else
                    param += c;
            };
        };
        eval(new_path + '=value');
    };

    function get_value(path) {
        var depth=0, new_path='', param, c;
        for (var idx = 0; idx < path.length; idx++) {
            c = path[idx];
            if (c === '[') {
                if (depth++ === 0) {
                    new_path += '[';
                    param = '';
                } else {
                    param += '[';
                };
            } else if (c === ']') {
                if (--depth === 0) {
                    new_path += JSON.stringify(eval(param)) + ']';
                } else {
                    param += ']';
                };
            } else {
                if (c === '@') c = 'base.';
                if (depth === 0)
                    new_path += c;
                else
                    param += c;
            };
        };
        return eval(new_path);
    };

    function katakana2hiragana(string){
        const baseHiragana = 12352 // start of unicode hiragana block
        const endHiragana = 12447 // end of unicode hiragana block
        const baseKatakana = 12448 // start of unicode katakana block
        const endKatakana = 12543 // end of unicode katakana block
        const delta = baseKatakana - baseHiragana;
        let result = []
        for (let idx in string){
            let x = string.charCodeAt(idx);
            if (x > baseKatakana && x <= endKatakana) x -= delta;
            x = String.fromCodePoint(x)
            result.push(x);
        };
        return result.join('');
    };

    function reportSearchResult(item, match, place){
        item.report = 'Search term '+match+' matches at '+place;
    };

    function prepareKanjidic2(filterValue){
        dataRequired.kanjidic2 = true;
        return loadMissingData();
    };

    function prepareKanjidic2AndWkRad(filterValue) {
        dataRequired.WkStrokeCountData = true;
        return prepareKanjidic2(filterValue);
    };

    // BEGIN Stroke Count GTEQ

    let strokeCountGteqHover_tip = 'Kanji and traditional radicals whose stroke count >= value';

    function registerStrokeCountGteqilter() {

        const registration = {
            type: 'number',
            label: 'Stroke Count &gt;=',
            default: 0,
            filter_func: strokeOrderGteqSearchFilter,
            filter_value_map: (x) => Number(x),
            prepare: prepareKanjidic2AndWkRad,
            set_options: function(options) { options.subjects = true;},
            hover_tip: strokeCountGteqHover_tip,
        };

        wkof.ItemData.registry.sources.wk_items.filters[strokeCountGteqFilterName] = $.extend({}, registration);
        wkof.ItemData.registry.sources.wk_items.filters[strokeCountGteqFilterName].alternate_sources = ['trad_rad'];
        wkof.ItemData.registry.sources.trad_rad.filters[strokeCountGteqFilterName] = $.extend({}, registration);
        wkof.ItemData.registry.sources.trad_rad.filters[strokeCountGteqFilterName].main_source = 'wk_items';

    };

    function strokeOrderGteqSearchFilter(filterValue, item){
        if (item.data === undefined) {
            return false;
        };
        let itemType = item.object;

        if (itemType === 'kanji' && (item.data.slug in kanjidic2Data)) {
            return Number(kanjidic2Data[item.data.slug].stroke_count) >= filterValue;
        } else if (itemType === 'radical') {
            return WkStrokeCountData[item.id].stroke_count >= filterValue;
        } else if (itemType === 'trad_rad') {
            if (typeof item.data.stroke_count === 'string'){
                return Number(item.data.stroke_count) >= filterValue;
            };
        };

        return false;
    };

    // END Stroke Count GTEQ

    // BEGIN Stroke Count LTEQ

    let strokeCountLteqHover_tip = 'Kanji and traditional radicals whose stroke count <= value';

    function registerStrokeCountLteqilter() {

        const registration = {
            type: 'number',
            label: 'Stroke Count &lt;=',
            default: 100,
            filter_func: strokeOrderLteqSearchFilter,
            filter_value_map: (x) => Number(x),
            prepare: prepareKanjidic2AndWkRad,
            set_options: function(options) { options.subjects = true;},
            hover_tip: strokeCountLteqHover_tip,
        };

        wkof.ItemData.registry.sources.wk_items.filters[strokeCountLteqFilterName] = $.extend({}, registration);
        wkof.ItemData.registry.sources.wk_items.filters[strokeCountLteqFilterName].alternate_sources = ['trad_rad'];
        wkof.ItemData.registry.sources.trad_rad.filters[strokeCountLteqFilterName] = $.extend({}, registration);
        wkof.ItemData.registry.sources.trad_rad.filters[strokeCountLteqFilterName].main_source = 'wk_items';

    };

    function strokeOrderLteqSearchFilter(filterValue, item){
        if (item.data === undefined) {
            return false;
        };
        let itemType = item.object;

        if (itemType === 'kanji' && item.data.characters in kanjidic2Data) {
            return Number(kanjidic2Data[item.data.characters].stroke_count) <= filterValue;
        } else if (itemType === 'radical') {
            return WkStrokeCountData[item.id].stroke_count <= filterValue;
        } else if (itemType === 'trad_rad') {
            if (typeof item.data.stroke_count === 'string'){
                return Number(item.data.stroke_count) <= filterValue;
            };
        };

        return false;
    };

    // END Stroke Count LTEQ

    // BEGIN Extensive Search

    let extensiveSearchHover_tip = 'Search for terms in meanings, readings and kanji.\nKanjidic2 meaning and readings are searched.\nYou may use latin, kana and kanji.';

    function registerExtensiveSearchFilter() {

        const registration = {
            type: 'text',
            label: 'Extensive Search',
            default: '',
            filter_func: extensiveSearchFilter,
            filter_value_map: prepareExtensiveFilterValue,
            prepare: prepareKanjidic2,
            set_options: function(options) { options.subjects = true;},
            hover_tip: extensiveSearchHover_tip,
        };

        wkof.ItemData.registry.sources.wk_items.filters[extensiveSearchFilterName] = $.extend({}, registration);
        wkof.ItemData.registry.sources.wk_items.filters[extensiveSearchFilterName].alternate_sources = ['trad_rad'];
        wkof.ItemData.registry.sources.trad_rad.filters[extensiveSearchFilterName] = $.extend({}, registration);
        wkof.ItemData.registry.sources.trad_rad.filters[extensiveSearchFilterName].main_source = 'wk_items';

    };

    function prepareExtensiveFilterValue(filterValue){
        return split_list(filterValue);
    };

    function extensiveSearchFilter(filterValue, item){
        if (item.data === undefined) {
            return false;
        };
        let itemType = item.object;

        for (var searchTerm of filterValue){
            if (item.data.characters !== null && item.data.characters.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'characters'); return true;};
            if (itemType === 'radical' && item.data.slug.indexOf(searchTerm.replace(' ', '-')) >= 0){reportSearchResult(item, searchTerm, 'characters'); return true;};

            for (var meaning of item.data.meanings){
                let term = meaning.meaning.toLowerCase();
                if (term.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'meanings'); return true;};
            };

            if (itemType !== 'radical' && itemType !== 'kana_vocabulary'){
                for (let reading of item.data.readings){
                    if (reading.reading.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'readings'); return true;};
                };
            };

            if (itemType === 'kanji' && item.data.characters in kanjidic2Data){
                for (let meaning of kanjidic2Data[item.data.characters].meanings){
                    let term = meaning.toLowerCase();
                    if (term.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'kanjidic2 meaning'); return true;};
                };

                for (let onyomi of kanjidic2Data[item.data.characters].onyomi){
                    let term = katakana2hiragana(onyomi);
                    if (term.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'kanjidic2 onyomi'); return true;};
                };

                for (let kunyomi of kanjidic2Data[item.data.characters].kunyomi){
                    let term = kunyomi
                    if (term.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'kanjidic2 kunyomi'); return true;};
                };

                for (let nanori of kanjidic2Data[item.data.characters].nanori){
                    let term = nanori
                    if (term.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'kanjidic2 nanori'); return true;};
                };
            };
        };
        return false;
    };

    // END Extensive Search

    // BEGIN Advanced Search

    let advancedSearchHover_tip = 'Search for terms in multiple locations of your choice.\nYou may use latin, kana and kanji.\nMultiple search options permit to tailor your search.';

    function registerAdvancedSearchFilter() {

        const registration = {
            type: 'button',
            label: 'Advanced Search',
            default: advancedSearchDefaults,
            callable_dialog: true,
            on_click: advancedSearchDialog,
            filter_func: advancedSearchFilter,
            filter_value_map: prepareFilterValue,
            prepare: prepareAdvancedSearch,
            set_options: function(options) { options.subjects = true; options.study_materials = true; },
            hover_tip: advancedSearchHover_tip,
        };

        wkof.ItemData.registry.sources.wk_items.filters[advancedSearchFilterName] = $.extend({}, registration);
        wkof.ItemData.registry.sources.wk_items.filters[advancedSearchFilterName].alternate_sources = ['trad_rad'];
        wkof.ItemData.registry.sources.trad_rad.filters[advancedSearchFilterName] = $.extend({}, registration);
        wkof.ItemData.registry.sources.trad_rad.filters[advancedSearchFilterName].main_source = 'wk_items';

    };


    let advancedSearchDefaults = {itemType: {radical: true, kanji: true, vocabulary: true, kana_vocabulary: true, trad_rad: true,},
                                  exactMatch: {characters: true, meanings: false, readings: true,
                                               allow: false, block: false,
                                               mMnemonics: false, mHints: false, rMnemonics: false, rHints: false, contextSentences: false,
                                               mNotes: false, rNotes: false, synonyms: false,
                                               Kanjidic2_Meaning: false, Kanjidic2_Onyomi: false,
                                               Kanjidic2_Kunyomi: false, Kanjidic2_Nanori: false},
                                  acceptedAnswer: true,
                                  searchIn: {characters: true, meanings: true, readings: true, onyomi: true, kunyomi: true, nanori: true,
                                             pos: false, allow: false, block: false,
                                             mMnemonics: false, mHints: false, rMnemonics: false, rHints: false, contextSentences: false,
                                             mNotes: false, rNotes: false, synonyms: false,
                                             Kanjidic2_Meaning: false, Kanjidic2_Onyomi: false,
                                             Kanjidic2_Kunyomi: false, Kanjidic2_Nanori: false},
                                  keisei: {phonetic: false, nonPhonetic: false, compound: false,
                                           notCompound: false, obscure: false, notInDB: false},
                                  searchTerms: ''};


    function advancedSearchDialog(name, config, on_change){
        let $originalDialog = $('#wkof_ds').find('[role="dialog"]');
        let areaId = settingsScriptId+'_advSearch';
        let scriptId = config.script_id || $originalDialog.attr("aria-describedby").slice(6);
        let path;
        if (config.path){
            path = config.path.replaceAll('@', 'wkof.settings["'+scriptId+'"].');
        } else if (scriptId === 'ss_quiz') {

            // Self Study Quiz doesn't define the path but we know what it is provided we find the source
            let row = $(this.delegateTarget);
            let panel = row.closest('[role="tabpanel"]');
            let source = panel.attr('id').match(/^ss_quiz_pg_(.*)$/)[1];
            path = 'wkof.settings.ss_quiz.ipresets[wkof.settings.ss_quiz.active_ipreset].content.'+source+'.filters.advSearchFilters_advancedSearch.value'

            // initialize in case it is not already initialized
            let v = $.extend(true, {}, get_value(path));
            set_value(path, v)
        } else {
            throw 'config.path is not defined';
        };

        let value = $.extend(true, {}, advancedSearchDefaults, get_value(path));
        set_value(path, value)
        wkof.settings[settingsScriptId] = wkof.settings[settingsScriptId] || {};
        wkof.settings[settingsScriptId].advSearch = $.extend(true, {}, advancedSearchDefaults, get_value(path));

        let html = '<textarea id="'+areaId+'" rows="2"></textarea>';
        let searchTermHovertip = ''+
            'List your search terms separated by commas. Latin, hiragana and kanji accepted\n\n'+
            '*  selects items where "Search In" information is PRESENT *and* NON EMPTY.\n'+
            '!* selects items where "Search In" information is ABSENT *or* EMPTY.\n\n'+
            'Valid parts of speech are:\n'+
            'adjective adverb conjunction counter expression godan verb  ichican verb\n'+
            'interjection intransitive verb noun numeral prefix pronoun proper noun suffix\n'+
            'transitive verb い adjective な adjective の adjective する verb';

        let dialogConfig = {
            script_id: settingsScriptId,
            title: 'Advanced Search',
            on_save: on_save,
            on_cancel: on_cancel,
            on_close: on_close,
            no_bkgd: true,
            settings: {itemType: {type: 'list', multi: true, label: 'Item Type', path: '@advSearch.itemType',
                                  hover_tip: 'The type of items that must match the search terms.',
                                  default: {radical: true, kanji: true, vocabulary: true, kana_vocabulary: true, trad_rad: true},
                                  content: {radical: 'Radical', kanji: 'Kanji', vocabulary: 'Vocabulary', kana_vocabulary: 'Kana Vocabulary',
                                            trad_rad:'Traditional Radical'},},
                       exactMatch: {type: 'list', multi: true, label: 'Searches With Exact Match', size: 5, path: '@advSearch.exactMatch',
                                    hover_tip: 'Selected searches requires the whole word matches.\nSubstring match is the default\nPart of Speech always use exact match.\n\nYou must select the search in Search In\nfor this parameter to take effect.',
                                    default: {characters: true, meanings: false, readings: true,
                                              allow: false, block: false,
                                              mMnemonics: false, mHints: false, rMnemonics: false, rHints: false, contextSentences: false,
                                              mNotes: false, rNotes: false, synonyms: false,
                                              Kanjidic2_Meaning: false, Kanjidic2_Onyomi: false,
                                              Kanjidic2_Kunyomi: false, Kanjidic2_Nanori: false},
                                    content: {characters: 'Characters', meanings: 'Meanings', readings: 'Readings',
                                              allow: 'Allow List', block: 'Block list',
                                              mMnemonics: 'Meaning mnemonics', mHints: 'Meaning hints',
                                              rMnemonics: 'Reading mnemonics', rHints: 'Reading hints',
                                              contextSentences: 'Context Sentences',
                                              mNotes: 'Meaning notes', rNotes: 'Reading notes', synonyms: 'User synonyms',
                                              Kanjidic2_Meaning: 'Kanjidict2 Meaning', Kanjidic2_Onyomi: 'Kanjidict2 Onyomi',
                                              Kanjidic2_Kunyomi: 'Kanjidict2 Kunyomi', Kanjidic2_Nanori: 'Kanjidict2 Nanori'},},
                       acceptedAnswer: {type: 'checkbox', label: 'Only Accepted Answers', default: true, path: '@advSearch.acceptedAnswer',
                                        hover_tip: 'Match only meanings and readings marked\nas accepted answers by Wanikani.\nUnchecked, matches both accepted\nand unaccepted answers.',},
                       searchIn: {type: 'list', multi: true, label: 'Search In', size: 6, path: '@advSearch.searchIn',
                                  hover_tip: 'Where to search to find a search term match.\nAny reading applies to both kanji and vocabulary.\nOther readings apply only to kanji.',
                                  default: {characters: true, meanings: true, readings: true,
                                            onyomi: true, kunyomi: true, nanori: true,
                                            pos: false, allow: false, block: false,
                                            mMnemonics: false, mHints: false, rMnemonics: false, rHints: false, contextSentences: false,
                                            mNotes: false, rNotes: false, synonyms: false,
                                            Kanjidic2_Meaning: false, Kanjidic2_Onyomi: false,
                                            Kanjidic2_Kunyomi: false, Kanjidic2_Nanori: false},
                                  content: {characters: 'Characters', meanings: 'Meanings', readings: 'Any Reading',
                                            onyomi: 'Readings Onyomi', kunyomi: 'Readings Kunyomi', nanori: 'Readings Nanori',
                                            pos: 'Part of Speech', allow: 'Allow List', block: 'Block List',
                                            mMnemonics: 'Meaning Mnemonics', mHints: 'Meaning Hints',
                                            rMnemonics: 'Reading Mnemonics', rHints: 'Reading Hints',
                                            contextSentences: 'Context Sentences',
                                            mNotes: 'Meaning Notes', rNotes: 'Reading Notes', synonyms: 'User Synonyms',
                                            Kanjidic2_Meaning: 'Kanjidict2 Meaning', Kanjidic2_Onyomi: 'Kanjidict2 Onyomi',
                                            Kanjidic2_Kunyomi: 'Kanjidict2 Kunyomi', Kanjidic2_Nanori: 'Kanjidict2 Nanori',},},
                       keisei: {type: 'list', multi: true, label: 'Search Keisei Database', size: 4, path: '@advSearch.keisei',
                                hover_tip: 'Selected items must have one of the selected Keisei Semantic-Phonetic properties.\n\nThis search criterion is not used if no property is selected.',
                                default: {phonetic: false, nonPhonetic: false, compound: false,
                                          notCompound: false, obscure: false, notInDB: false},
                                content: {phonetic: 'Is a Phonetic Mark', nonPhonetic: 'Is Not a Phonetic Mark',
                                          compound: 'Is a Phonetic Compound',
                                          notCompound: 'Not a Phonetic Compound', obscure: 'Obscure or Contested Origin',
                                          notInDB: 'Not in Keisei Database'},},
                       divider: {type: 'divider'},
                       searchTerms: {type: 'html', label: 'Search Terms',
                                     html: html,},
                      },
        };

        let dialog = new wkof.Settings(dialogConfig);
        dialog.open();

        // ui issue: do not let the calling dialog be visible
        let originalDisplay = $originalDialog.css('display');
        $originalDialog.css('display', 'none');

        // work around some framework limitations regarding html types
        let $searchTerms = $('#'+areaId);
        $searchTerms.val(wkof.settings[settingsScriptId].advSearch.searchTerms);
        $searchTerms.change(textareaChanged);
        let $label = $searchTerms.closest('form').children('.left');
        $label.css('width', 'calc(100% - 5px)');
        $label.children('label').css('text-align', 'left');
        $label.attr('title', searchTermHovertip);

        function on_close(){
            $originalDialog.css('display', originalDisplay);
            if (typeof config.on_close === 'function') config.on_close();
        };

        function on_save(){
            set_value(path, get_value('wkof.settings.'+settingsScriptId+'.advSearch'))
            if (typeof config.on_save === 'function') config.on_save();
        };

        function on_cancel(){
            if (typeof config.on_cancel === 'function') config.on_cancel();
        };

        function textareaChanged(e){
            wkof.settings[settingsScriptId].advSearch.searchTerms = $searchTerms.val();
        };

    };

    function prepareAdvancedSearch(filterValueOriginal){
        let searchIn = filterValueOriginal.searchIn;
        dataRequired.kanjidic2 = (searchIn.Kanjidic2_Meaning || searchIn.Kanjidic2_Onyomi || searchIn.Kanjidic2_Kunyomi || searchIn.Kanjidic2_Nanori);
        let keisei = filterValueOriginal.keisei;
        dataRequired.keisei = (keisei.phonetic || keisei.nonPhonetic || keisei.compound || keisei.notCompound || keisei.obscure || keisei.notInDB);
        return loadMissingData();
    };

    function prepareFilterValue(filterValueOriginal){
        // Don't modify the original because it is a pointer to the filter settings - modifying it will accumulate trash in the settings
        var filterValue = $.extend({}, filterValueOriginal);
        filterValue.hasSearchIn = Object.values(filterValue.searchIn).reduce(((acc,cur) => acc || cur), false);
        filterValue.searchIn.keisei = Object.values(filterValue.keisei).reduce(((acc, cur) => acc || cur), false);
        filterValue.searchTermsArray = split_list(filterValue.searchTerms);
        return filterValue;
    };

    function advancedSearchFilter(filterValue, item) {
        if (item.data === undefined) {
            return false;
        };
        if (!filterValue.itemType[item.object]) return false;

        let searchIn = filterValue.searchIn;
        let exactMatch = filterValue.exactMatch;
        let keisei = filterValue.keisei;
        let hasSearchIn = filterValue.hasSearchIn;
        let acceptedAnswer = filterValue.acceptedAnswer;
        let itemType = item.object;

        for (var searchTerm of filterValue.searchTermsArray){

            // must match keisei if searched in AND match ANY ONE of the other searched in locations
            if (searchIn.keisei){
                let result = false;
                if (itemType === 'radical'){
                    if (searchTerm !== none) {
                        if (keiseiDB.checkPhonetic(keiseiDB.mapWKRadicalToPhon(item.data.slug))) {
                            if (keisei.phonetic) result = true;
                        } else {
                            if (keisei.nonPhonetic) result = true;
                        };
                    };
                } else if (itemType === 'kanji'){
                    if (searchTerm !== none) {
                        let kan = item.data.slug;
                        if (!keiseiDB.checkKanji(kan)){
                            if (keisei.notInDB) result = true;
                        } else if (!keiseiDB.checkPhonetic(kan) && (keiseiDB.getKType(kan) !== keiseiDB.KTypeEnum.comp_phonetic)){
                            let typeKan = keiseiDB.getKType(kan);
                            if (keisei.nonPhonetic || keisei.notCompound) {
                                result = true;
                            } if (!(typeKan in Object.values(keiseiDB.KTypeEnum))) {
                                if (keisei.notInDB) result = true;
                            } else if (typeKan === keiseiDB.KTypeEnum.unknown) {
                                if (keisei.obscure) result = true;
                            };
                        } else {
                            if (keiseiDB.checkPhonetic(kan)) {
                                if (keisei.phonetic || keisei.notCompound) result = true;
                            } else if ((keisei.compound || (keisei.nonPhonetic))){
                                result = true;
                            };
                        };
                    };
                };
                if (!result) return false;
                if (!hasSearchIn) return result; // if no other location is searched in we are done with the result
            };

            if (searchIn.characters){
                if (searchTerm === all) {reportSearchResult(item, searchTerm, 'characters'); return true;};
                if (searchTerm !== none) {
                    if (exactMatch.characters){
                        if (item.data.characters !== null && item.data.characters === searchTerm) {reportSearchResult(item, searchTerm, 'characters'); return true;};
                        if (itemType === 'radical' && item.data.slug === searchTerm.replace(' ', '-')) {reportSearchResult(item, searchTerm, 'characters'); return true;};
                    } else {
                        if (item.data.characters !== null && item.data.characters.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'characters'); return true;};
                        if (itemType === 'radical' && item.data.slug.indexOf(searchTerm.replace(' ', '-')) >= 0){reportSearchResult(item, searchTerm, 'characters'); return true;};
                    };
                };
            };

            if (searchIn.meanings) {
                if (searchTerm === all)  {reportSearchResult(item, searchTerm, 'meanings'); return true;};
                if (searchTerm !== none) {
                    for (var meaning of item.data.meanings){
                        if (acceptedAnswer && !meaning.accepted_answer) continue;
                        let term = meaning.meaning.toLowerCase();
                        if (exactMatch.meanings){
                            if (searchTerm === term) {reportSearchResult(item, searchTerm, 'meanings'); return true;};
                            let words = term.split(' ').filter(function(name) {return (name.length > 0);});
                            for (let word of words) {
                                if (searchTerm === word) {reportSearchResult(item, searchTerm, 'meanings'); return true;};
                            };
                        } else {
                            if (term.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'meanings'); return true;};
                        };
                    };
                };
            };

            if (searchIn.readings){
                if (itemType !== 'radical' && itemType !== 'kana_vocabulary'){
                    if (searchTerm === all) {reportSearchResult(item, searchTerm, 'readings'); return true;};
                    if (exactMatch.readings){
                        for (let reading of item.data.readings){
                            if (acceptedAnswer && !reading.accepted_answer) continue;
                            if (reading.reading === searchTerm && reading.accepted_answer) {reportSearchResult(item, searchTerm, 'readings'); return true;};
                        };
                    } else {
                        for (let reading of item.data.readings){
                            if (acceptedAnswer && !reading.accepted_answer) continue;
                            if (reading.reading.indexOf(searchTerm) >= 0 && reading.accepted_answer) {reportSearchResult(item, searchTerm, 'readings'); return true;};
                        };
                    };
                } else {
                    if (searchTerm === none) {reportSearchResult(item, searchTerm, 'readings');}
                };
            };

            if (searchIn.onyomi || searchIn.kunyomi || searchIn.nanori){
                if (itemType === 'kanji') {
                    let notSeen = {onyomi: true, kunyomi: true, nanori: true};
                    let seen = {onyomi: false, kunyomi: false, nanori: false};
                    if (exactMatch.readings){
                        for (let reading of item.data.readings){
                            if (acceptedAnswer && !reading.accepted_answer) continue;
                            notSeen[reading.type] = false;
                            seen[reading.type] = true;
                            if (reading.reading === searchTerm && searchIn[reading.type]) {reportSearchResult(item, searchTerm, reading.type); return true;};
                        };
                    } else {
                        for (let reading of item.data.readings){
                            if (acceptedAnswer && !reading.accepted_answer) continue;
                            notSeen[reading.type] = false;
                            seen[reading.type] = true;
                            if (reading.reading.indexOf(searchTerm) >= 0 && searchIn[reading.type]) {reportSearchResult(item, searchTerm, reading.type); return true;};
                        };
                    };
                    if (searchTerm === none){
                        if (searchIn.onyomi && notSeen.onyomi) {reportSearchResult(item, searchTerm, 'onyomi'); return true;};
                        if (searchIn.kunyomi && notSeen.kunyomi) {reportSearchResult(item, searchTerm, 'kunyomi'); return true;};
                        if (searchIn.nanori && notSeen.nanori) {reportSearchResult(item, searchTerm, 'nanori'); return true;};
                    };
                    if (searchTerm === all){
                        if (searchIn.onyomi && seen.onyomi) {reportSearchResult(item, searchTerm, 'onyomi'); return true;};
                        if (searchIn.kunyomi && seen.kunyomi) {reportSearchResult(item, searchTerm, 'kunyomi'); return true;};
                        if (searchIn.nanori && seen.nanori) {reportSearchResult(item, searchTerm, 'nanori'); return true;};
                    };
                } else {
                    let reading;
                    if (searchIn.nanori) reading = 'nanori';
                    if (searchIn.kunyomi) reading = 'kunyomi';
                    if (searchIn.onyomi) reading = 'onyomi';
                    if (searchTerm === none)  {reportSearchResult(item, searchTerm, reading); return true;};
                };
            };

            if (searchIn.pos){
                if (itemType === 'vocabulary' || itemType === 'kana_vocabulary'){
                    if (searchTerm === all) {reportSearchResult(item, searchTerm, 'part of speech'); return true;};
                    for (let pos of item.data.parts_of_speech){
                        if (searchTerm === pos) {reportSearchResult(item, searchTerm, 'part of speech'); return true;};
                        let words = pos.split(' ').filter(function(name) {return (name.length > 0);});
                        for (let word of words) {
                            if (searchTerm === word) {reportSearchResult(item, searchTerm, 'part of speech'); return true;};
                        };
                    };
                } else {
                    if (searchTerm === none) {reportSearchResult(item, searchTerm, 'part of speech'); return true;};
                };
            };

            if (searchIn.allow || searchIn.block) {
                if (itemType !== 'trad_rad'){
                    let seenAllow = false;
                    let seenBlock = false;
                    for (let list of item.data.auxiliary_meanings){
                        if (searchIn.allow && list.type === 'whitelist'){
                            if (searchTerm === all) {reportSearchResult(item, searchTerm, 'allow list'); return true;};
                            seenAllow = true;
                            if (exactMatch.allow){
                                if (searchTerm === list.meaning.toLowerCase()) {reportSearchResult(item, searchTerm, 'allow list'); return true;};
                                let words = list.meaning.toLowerCase().split(' ').filter(function(name) {return (name.length > 0);});
                                for (let word of words) {
                                    if (searchTerm === word) {reportSearchResult(item, searchTerm, 'allow list'); return true;};
                                };
                            } else {
                                if (list.meaning.toLowerCase().indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'allow list'); return true;};
                            };
                        } else if(searchIn.block && list.type === 'blacklist') {
                            if (searchTerm === all) {reportSearchResult(item, searchTerm, 'block list'); return true;};
                            seenBlock = true;
                            if (exactMatch.block){
                                if (searchTerm === list.meaning.toLowerCase()) {reportSearchResult(item, searchTerm, 'block list'); return true;};
                                let words = list.meaning.toLowerCase().split(' ').filter(function(name) {return (name.length > 0);});
                                for (let word of words) {
                                    if (searchTerm === word) {reportSearchResult(item, searchTerm, 'block list'); return true;};
                                };
                            } else {
                                if (list.meaning.toLowerCase().indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'block list'); return true;};
                            };
                        };
                    };
                    if (searchTerm === none){
                        if (searchIn.allow && !seenAllow) {reportSearchResult(item, searchTerm, 'allow list'); return true;};
                        if (searchIn.block && !seenBlock) {reportSearchResult(item, searchTerm, 'block list'); return true;};
                    };
                } else {
                    if (searchTerm === none){
                        if (searchIn.allow) {reportSearchResult(item, searchTerm, 'allow list'); return true;};
                        if (searchIn.block) {reportSearchResult(item, searchTerm, 'block list'); return true;};
                    };
                };
            };

            if (searchIn.mMnemonics) {
                if (item.data.meaning_mnemonic){
                    if (searchTerm === all) {reportSearchResult(item, searchTerm, 'meaning mnemonic'); return true;};
                    let mnemonic = item.data.meaning_mnemonic.toLowerCase().replace(noHtml, ' ');
                    if (searchTerm === "onyomi" && mnemonic.indexOf("on'yomi") >= 0) {reportSearchResult(item, searchTerm, 'meaning mnemonic'); return true;};
                    if (searchTerm === "kunyomi" && mnemonic.indexOf("kun'yomi") >= 0) {reportSearchResult(item, searchTerm, 'meaning mnemonic'); return true;};
                    if (exactMatch.mMnemonics){
                        let words = mnemonic.toLowerCase().split(breakTheWords).filter(function(name) {return (name.length > 0);});
                        for (let word of words) {
                            if (searchTerm === word) {reportSearchResult(item, searchTerm, 'meaning mnemonic'); return true;};
                            let subwords = word.split(breakTheSubwords);
                            if (subwords.length !== 1){
                                for (let subword of subwords) if (searchTerm === subword) {reportSearchResult(item, searchTerm, 'meaning mnemonic'); return true;};
                            };
                        };
                    } else {
                        if (mnemonic.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'meaning mnemonic'); return true;};
                    };
                } else {
                    if (searchTerm === none) {reportSearchResult(item, searchTerm, 'meaning mnemonic'); return true;};
                };
            };

            if (searchIn.mHints){
                if (item.data.meaning_hint){
                    if (searchTerm === all) {reportSearchResult(item, searchTerm, 'meaning hint'); return true;};
                    let hint = item.data.meaning_hint.toLowerCase().replace(noHtml, ' ');
                    if (searchTerm === "onyomi" && hint.indexOf("on'yomi") >= 0) {reportSearchResult(item, searchTerm, 'meaning hint'); return true;};
                    if (searchTerm === "kunyomi" && hint.indexOf("kun'yomi") >= 0) {reportSearchResult(item, searchTerm, 'meaning hint'); return true;};
                    if (exactMatch.mHints){
                        let words = hint.split(breakTheWords).filter(function(name) {return (name.length > 0);});
                        for (let word of words) {
                            if (searchTerm === word) {reportSearchResult(item, searchTerm, 'meaning hint'); return true;};
                            let subwords = word.split(breakTheSubwords);
                            if (subwords.length !== 1){
                                for (let subword of subwords) if (searchTerm === subwords) {reportSearchResult(item, searchTerm, 'meaning hint'); return true;};
                            }
                        };
                    } else {
                        if (hint.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'meaning hint'); return true;};
                    };
                } else {
                    if (searchTerm === none) {reportSearchResult(item, searchTerm, 'meaning hint'); return true;};
                };
            };

            if (searchIn.rMnemonics) {
                if (item.data.reading_mnemonic){
                    if (searchTerm === all) {reportSearchResult(item, searchTerm, 'reading mnemonic'); return true;};
                    let mnemonic = item.data.reading_mnemonic.toLowerCase().replace(noHtml, ' ');
                    if (searchTerm === "onyomi" && mnemonic.indexOf("on'yomi") >= 0) {reportSearchResult(item, searchTerm, 'reading mnemonic'); return true;};
                    if (searchTerm === "kunyomi" && mnemonic.indexOf("kun'yomi") >= 0) {reportSearchResult(item, searchTerm, 'reading mnemonic'); return true;};
                    if (exactMatch.rMnemonics){
                        let words = mnemonic.split(breakTheWords).filter(function(name) {return (name.length > 0);});
                        for (let word of words) {
                            if (searchTerm === word) {reportSearchResult(item, searchTerm, 'reading mnemonic'); return true;};
                            let subwords = word.split(breakTheSubwords);
                            if (subwords.length !== 1){
                                for (let subword of subwords) if (searchTerm === subword) {reportSearchResult(item, searchTerm, 'reading mnemonic'); return true;};
                            }
                        };
                    } else {
                        if (mnemonic.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'reading mnemonic'); return true;};
                    };
                } else {
                    if (searchTerm === none) {reportSearchResult(item, searchTerm, 'reading mnemonic'); return true;};
                };
            };

            if (searchIn.rHints) {
                if (item.data.reading_hint){
                    if (searchTerm === all) {reportSearchResult(item, searchTerm, 'reading hint'); return true;};
                    let hint = item.data.reading_hint.toLowerCase().replace(noHtml, ' ');
                    if (searchTerm === "onyomi" && hint.indexOf("on'yomi") >= 0) {reportSearchResult(item, searchTerm, 'reading hint'); return true;};
                    if (searchTerm === "kunyomi" && hint.indexOf("kun'yomi") >= 0) {reportSearchResult(item, searchTerm, 'reading hint'); return true;};
                    if (exactMatch.rHints){
                        let words = hint.split(breakTheWords).filter(function(name) {return (name.length > 0);});
                        for (let word of words) {
                            if (searchTerm === word) {reportSearchResult(item, searchTerm, 'reading hint'); return true;};
                            let subwords = word.split(breakTheSubwords);
                            if (subwords.length !== 1){
                                for (let subword of subwords) if (searchTerm === subword) {reportSearchResult(item, searchTerm, 'reading hint'); return true;};
                            }
                        };
                    } else {
                        if (hint.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'reading hint'); return true;};
                    };
                } else {
                    if (searchTerm === none) {reportSearchResult(item, searchTerm, 'reading hint'); return true;};
                };
            };

            if (searchIn.contextSentences){
                if (item.object === 'vocabulary'){
                    if (item.data.context_sentences.length !== 0){
                        if (searchTerm === all) {reportSearchResult(item, searchTerm, 'context sentences'); return true;};
                        for (let sentences of item.data.context_sentences){
                            let ja = sentences.ja;
                            let en = sentences.en;
                            if (exactMatch.contextSentences){
                                let words = ja.toLowerCase().split(breakTheWords).filter(function(name) {return (name.length > 0);});
                                for (let word of words) {
                                    if (searchTerm === word) {reportSearchResult(item, searchTerm, 'context sentences'); return true;};
                                };
                                words = en.toLowerCase().split(breakTheWords).filter(function(name) {return (name.length > 0);});
                                for (let word of words) {
                                    if (searchTerm === word) {reportSearchResult(item, searchTerm, 'context sentences'); return true;};
                                    let subwords = word.split(breakTheSubwords);
                                    if (subwords.length !== 1){
                                        for (let subword of subwords) if (searchTerm === subword) {reportSearchResult(item, searchTerm, 'context sentences'); return true;};
                                    };
                                };
                            } else {
                                if (ja.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'context sentences'); return true;};
                                if (en.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'context sentences'); return true;};
                            };
                        };
                    } else {
                        if (searchTerm === none) {reportSearchResult(item, searchTerm, 'context sentences'); return true;};
                    };
                } else {
                    if (searchTerm === none) {reportSearchResult(item, searchTerm, 'context sentences'); return true;};
                };
            };

            if (searchIn.mNotes){
                if (item.study_materials){
                    if (item.study_materials.meaning_note){
                        if (searchTerm === all) {reportSearchResult(item, searchTerm, 'meaning note'); return true;};
                        let note = item.study_materials.meaning_note.toLowerCase().replace(noHtml, ' ');
                        if (searchTerm === "onyomi" && note.indexOf("on'yomi") >= 0) {reportSearchResult(item, searchTerm, 'meaning note'); return true;};
                        if (searchTerm === "kunyomi" && note.indexOf("kun'yomi") >= 0) {reportSearchResult(item, searchTerm, 'meaning note'); return true;};
                        if (exactMatch.mNotes){
                            let words = note.split(breakTheWords).filter(function(name) {return (name.length > 0);});
                            for (let word of words) {
                                if (searchTerm === word) {reportSearchResult(item, searchTerm, 'meaning note'); return true;};
                                let subwords = word.split(breakTheSubwords);
                                if (subwords.length !== 1){
                                    for (let subword of subwords) if (searchTerm === subword) {reportSearchResult(item, searchTerm, 'meaning note'); return true;};
                                };
                            };
                        } else {
                            if (note.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'meaning note'); return true;};
                        };
                    } else {
                        if (searchTerm === none) {reportSearchResult(item, searchTerm, 'meaning note'); return true;};
                    };
                } else {
                    if (searchTerm === none) {reportSearchResult(item, searchTerm, 'meaning note'); return true;};
                };
            };

            if (searchIn.rNotes){
                if (item.study_materials){
                    if (item.study_materials.reading_note){
                        if (searchTerm === all) {reportSearchResult(item, searchTerm, 'reading note'); return true;};
                        let note = item.study_materials.reading_note.toLowerCase().replace(noHtml, ' ');
                        if (searchTerm === "onyomi" && note.indexOf("on'yomi") >= 0) {reportSearchResult(item, searchTerm, 'reading note'); return true;};
                        if (searchTerm === "kunyomi" && note.indexOf("kun'yomi") >= 0) {reportSearchResult(item, searchTerm, 'reading note'); return true;};
                        if (exactMatch.rNotes){
                            let words = note.split(breakTheWords).filter(function(name) {return (name.length > 0);});
                            for (let word of words) {
                                if (searchTerm === word) {reportSearchResult(item, searchTerm, 'reading note'); return true;};
                                let subwords = word.split(breakTheSubwords);
                                if (subwords.length !== 1){
                                    for (let subword of subwords) if (searchTerm === subword) {reportSearchResult(item, searchTerm, 'reading note'); return true;};
                                }
                            };
                        } else {
                            if (note.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'reading note'); return true;};
                        };
                    } else {
                        if (searchTerm === none) {reportSearchResult(item, searchTerm, 'reading note'); return true;};
                    };
                } else {
                    if (searchTerm === none) {reportSearchResult(item, searchTerm, 'reading note'); return true;};
                };
            };

            if (searchIn.synonyms){
                if (item.study_materials){
                    let synonyms = item.study_materials.meaning_synonyms;
                    if (synonyms.length === 0){
                        if (searchTerm === none) {reportSearchResult(item, searchTerm, 'user synonyms'); return true;};
                    } else {
                        if (searchTerm === all) {reportSearchResult(item, searchTerm, 'user synonyms'); return true;};
                        for (let synonym of synonyms){
                            synonym = synonym.toLowerCase();
                            if (searchTerm === synonym) {reportSearchResult(item, searchTerm, 'user synonyms'); return true;};
                            if (exactMatch.synonyms){
                                let words = synonym.split(breakTheWords).filter(function(name) {return (name.length > 0);});
                                for (var word of words) {
                                    if (searchTerm === word) {reportSearchResult(item, searchTerm, 'user synonyms'); return true;};
                                };
                                let subwords = word.split(breakTheSubwords);
                                if (subwords.length !== 1){
                                    for (let subword of subwords) if (searchTerm === subword) {reportSearchResult(item, searchTerm, 'user synonyms'); return true;};
                                }
                            } else {
                                if (synonym.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'user synonyms'); return true;};
                            };
                        };
                    };
                } else {
                    if (searchTerm === none) {reportSearchResult(item, searchTerm, 'user synonyms'); return true;};
                };
            };

            if (searchIn.Kanjidic2_Meaning) {
                if (item.object !== 'kanji'){
                    if (searchTerm === none) {reportSearchResult(item, searchTerm, 'kanjidic2 meaning'); return true;};
                } else {
                    if (searchTerm === all) {
                        if (item.data.characters in kanjidic2Data && kanjidic2Data[item.data.characters].meanings.length !== 0){
                            reportSearchResult(item, searchTerm, 'kanjidic2 meaning');
                            return true;
                        };
                    } else if (searchTerm === none) {
                        if (!(item.data.characters in kanjidic2Data) || kanjidic2Data[item.data.characters].meanings.length === 0){
                            reportSearchResult(item, searchTerm, 'kanjidic2 meaning');
                            return true;
                        };
                    } else if (item.data.characters in kanjidic2Data){
                        for (let meaning of kanjidic2Data[item.data.characters].meanings){
                            let term = meaning.toLowerCase();
                            if (exactMatch.Kanjidic2_Meaning){
                                if (searchTerm === term) {reportSearchResult(item, searchTerm, 'kanjidic2 meaning'); return true;};
                                let words = term.split(' ').filter(function(name) {return (name.length > 0);});
                                for (let word of words) {
                                    if (searchTerm === word) {reportSearchResult(item, searchTerm, 'kanjidic2 meaning'); return true;};
                                };
                            } else {
                                if (term.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'kanjidic2 meaning'); return true;};
                            };
                        };
                    };
                };
            };

            if (searchIn.Kanjidic2_Onyomi) {
                if (item.object !== 'kanji'){
                    if (searchTerm === none) {reportSearchResult(item, searchTerm, 'kanjidic2 onyomi'); return true;};
                } else {
                    if (searchTerm === all) {
                        if (item.data.characters in kanjidic2Data && kanjidic2Data[item.data.characters].onyomi.length !== 0){
                            reportSearchResult(item, searchTerm, 'kanjidic2 onyomi');
                            return true;
                        };
                    } else if (searchTerm === none) {
                        if (!(item.data.characters in kanjidic2Data) || kanjidic2Data[item.data.characters].onyomi.length === 0){
                            reportSearchResult(item, searchTerm, 'kanjidic2 onyomi');
                            return true;
                        };
                    } else if (item.data.characters in kanjidic2Data){
                        for (let onyomi of kanjidic2Data[item.data.characters].onyomi){
                            let term = katakana2hiragana(onyomi);
                            if (exactMatch.Kanjidic2_Onyomi){
                                if (searchTerm === term) {reportSearchResult(item, searchTerm, 'kanjidic2 onyomi'); return true;};
                                let words = term.split(' ').filter(function(name) {return (name.length > 0);});
                                for (let word of words) {
                                    if (searchTerm === word) {reportSearchResult(item, searchTerm, 'kanjidic2 onyomi'); return true;};
                                    if (word.endsWith('-') || word.endsWith('ー') || word.endsWith('ー') || word.endsWith('-') || word.endsWith('‐')){
                                        term = word.slice(0, -1)
                                    };
                                    if (word.startsWith('-') || word.startsWith('ー') || word.startsWith('ー') || word.startsWith('-') || word.startsWith('‐')){
                                        term = word.slice(1)
                                    };
                                    if (searchTerm === term) {reportSearchResult(item, searchTerm, 'kanjidic2 onyomi'); return true;};
                                };
                            } else {
                                if (term.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'kanjidic2 onyomi'); return true;};
                            };
                        };
                    };
                };
            };

            if (searchIn.Kanjidic2_Kunyomi) {
                if (item.object !== 'kanji'){
                    if (searchTerm === none) {reportSearchResult(item, searchTerm, 'kanjidic2 kunyomi'); return true;};
                } else {
                    if (searchTerm === all) {
                        if (item.data.characters in kanjidic2Data && kanjidic2Data[item.data.characters].kunyomi.length !== 0){
                            reportSearchResult(item, searchTerm, 'kanjidic2 kunyomi');
                            return true;
                        };
                    } else if (searchTerm === none) {
                        if (!(item.data.characters in kanjidic2Data) || kanjidic2Data[item.data.characters].kunyomi.length === 0){
                            reportSearchResult(item, searchTerm, 'kanjidic2 kunyomi');
                            return true;
                        };
                    } else if (item.data.characters in kanjidic2Data){
                        for (let kunyomi of kanjidic2Data[item.data.characters].kunyomi){
                            let term = kunyomi
                            if (exactMatch.Kanjidic2_Kunyomi){
                                if (searchTerm === term) {reportSearchResult(item, searchTerm, 'kanjidic2 kunyomi'); return true;};
                                let words = term.split(' ').filter(function(name) {return (name.length > 0);});
                                for (let word of words) {
                                    if (searchTerm === word) {reportSearchResult(item, searchTerm, 'kanjidic2 kunyomi'); return true;};
                                    if (word.endsWith('-') || word.endsWith('ー') || word.endsWith('ー') || word.endsWith('-') || word.endsWith('‐')){
                                        term = word.slice(0, -1)
                                    };
                                    if (word.startsWith('-') || word.startsWith('ー') || word.startsWith('ー') || word.startsWith('-') || word.startsWith('‐')){
                                        term = word.slice(1)
                                    };
                                    if (searchTerm === term) {reportSearchResult(item, searchTerm, 'kanjidic2 kunyomi'); return true;};
                                };
                            } else {
                                if (term.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'kanjidic2 kunyomi'); return true;};
                            };
                        };
                    };
                };
            };

            if (searchIn.Kanjidic2_Nanori) {
                if (item.object !== 'kanji'){
                    if (searchTerm === none) {reportSearchResult(item, searchTerm, 'kanjidic2 nanori'); return true;};
                } else {
                    if (searchTerm === all) {
                        if (item.data.characters in kanjidic2Data && kanjidic2Data[item.data.characters].nanori.length !== 0){
                            reportSearchResult(item, searchTerm, 'kanjidic2 nanori');
                            return true;
                        };
                    } else if (searchTerm === none) {
                        if (!(item.data.characters in kanjidic2Data) || kanjidic2Data[item.data.characters].nanori.length === 0){
                            reportSearchResult(item, searchTerm, 'kanjidic2 nanori');
                            return true;
                        };
                    } else if (item.data.characters in kanjidic2Data){
                        for (let nanori of kanjidic2Data[item.data.characters].nanori){
                            let term = nanori
                            if (exactMatch.Kanjidic2_Nanori){
                                if (searchTerm === term) {reportSearchResult(item, searchTerm, 'kanjidic2 nanori'); return true;};
                                let words = term.split(' ').filter(function(name) {return (name.length > 0);});
                                for (let word of words) {
                                    if (searchTerm === word) {reportSearchResult(item, searchTerm, 'kanjidic2 nanori'); return true;};
                                    if (word.endsWith('-') || word.endsWith('ー') || word.endsWith('ー') || word.endsWith('-') || word.endsWith('‐')){
                                        term = word.slice(0, -1)
                                    };
                                    if (word.startsWith('-') || word.startsWith('ー') || word.startsWith('ー') || word.startsWith('-') || word.startsWith('‐')){
                                        term = word.slice(1)
                                    };
                                    if (searchTerm === term) {reportSearchResult(item, searchTerm, 'kanjidic2 nanori'); return true;};
                                };
                            } else {
                                if (term.indexOf(searchTerm) >= 0) {reportSearchResult(item, searchTerm, 'kanjidic2 nanori'); return true;};
                            };
                        };
                    };
                };
            };


        };

        return false;
    };

    // END Advanced Search

    // BEGIN Related Search
    let relatedSearchHover_tip = 'Find items related to search terms.\n\nRelationships include:\n* Components of items\n* Items where components are used\n* Visually similar kanjis\n* Semantic-phonetic composition relationships';

    function registerRelatedSearchFilter() {
        const registration = {
            type: 'button',
            label: 'Related Search',
            default: relatedSearch_defaults,
            callable_dialog: true,
            on_click: relatedSearchDialog,
            prepare: prepareRelatedSearch,
            filter_value_map: prepareRelatedFilterValue,
            filter_func: relatedSearchFilter,
            set_options: function(options) { options.subjects = true; },
            hover_tip: relatedSearchHover_tip,
        };

        wkof.ItemData.registry.sources.wk_items.filters[relatedSearchFilterName] = $.extend(true, {}, registration);
        wkof.ItemData.registry.sources.wk_items.filters[relatedSearchFilterName].alternate_sources = ['trad_rad'];
        wkof.ItemData.registry.sources.trad_rad.filters[relatedSearchFilterName] = $.extend(true, {}, registration);
        wkof.ItemData.registry.sources.trad_rad.filters[relatedSearchFilterName].main_source = 'wk_items';

    };

    let relatedSearch_defaults = {returns: {matchedItem: true, components: false, usedIn: true,
                                            WKvisuallySimilar: false, LYvisuallySimilar: false, NiaiVisuallySimilar: false, NiaiAlternate: 0.3,
                                            phonetic: false, nonPhonetic: false, keiseiRelated: false,
                                            tradRadInKanji: false, kanjiwithTradRad: false},
                                  similarityTreshold: 0,
                                  searchTerms: '',
                                  itemType: {radical: true, kanji: true, vocabulary: true, trad_rad: true,},
                                 };

    function relatedSearchDialog(name, config, on_change){
        let $originalDialog = $('#wkof_ds').find('[role="dialog"]');
        let areaId = settingsScriptId+'_relSearch';
        let scriptId = config.script_id || $originalDialog.attr("aria-describedby").slice(6);
        let path;
        if (config.path){
            path = config.path.replaceAll('@', 'wkof.settings["'+scriptId+'"].');
        } else if (scriptId === 'ss_quiz') {

            // Self Study Quiz doesn't define the path but we know what it is provided we find the source
            let row = $(this.delegateTarget);
            let panel = row.closest('[role="tabpanel"]');
            let source = panel.attr('id').match(/^ss_quiz_pg_(.*)$/)[1];
            path = 'wkof.settings.ss_quiz.ipresets[wkof.settings.ss_quiz.active_ipreset].content.'+source+'.filters.advSearchFilters_relatedSearch.value'

            // initialize in case it is not already initialized
            let v = $.extend(true, {}, get_value(path));
            set_value(path, v)
        } else {
            throw 'config.path is not defined';
        };

        let value = $.extend(true, {}, relatedSearch_defaults, get_value(path));
        set_value(path, value);
        wkof.settings[settingsScriptId] = wkof.settings[settingsScriptId] || {};
        wkof.settings[settingsScriptId].relSearch = $.extend(true, {}, relatedSearch_defaults, get_value(path));

        let html = '<textarea id="'+areaId+'" rows="3"></textarea>';
        let searchTermHovertip = ''+
            'List your search terms separated by commas. Latin, kana and kanji accepted.\n'+
            'Your search terms must match the characters for the item exactly.\n'+
            'English name of radicals and traditional radicals are accepted.';

        let dialogConfig = {
            script_id: settingsScriptId,
            title: 'Related Search',
            on_save: on_save,
            on_cancel: on_cancel,
            on_close: on_close,
            no_bkgd: true,
            settings: {returns: {type: 'list', multi: true, label: 'Items Returned By The Search', size: 4, path: '@relSearch.returns',
                                 hover_tip: 'Choose which items are being searched.\nWK Visially similar means according to Wanikani data.\nLY Visually similar means according to Lars Yencken data.',
                                 default: {matchedItem: true, components: false, usedIn: true,
                                           WKvisuallySimilar: false, LYvisuallySimilar: false, NiaiVisuallySimilar: false, NiaiAlternate: 0.3,
                                           phonetic: false, nonPhonetic: false, keiseiRelated: false,
                                           tradRadInKanji: false, kanjiwithTradRad: false},
                                 content: {matchedItem: 'Matched Items', components: 'Components of Matched Items', usedIn: 'Items Where Matches Are Used',
                                           WKvisuallySimilar: 'WK Visually Similar To Matches', LYvisuallySimilar: 'LY Visually Similar To Matches',
                                           NiaiVisuallySimilar: 'Niai Visually Similar To Matches',
                                           phonetic: 'Keisei Phonetic Compounds', nonPhonetic: 'Non Phonetic Compounds',
                                           keiseiRelated: 'Related Phonetic Marks',
                                           tradRadInKanji: 'Traditional Radicals in Kanji', kanjiwithTradRad: 'Kanji Using Traditional Radicals'},},
                       LYsimilarityTreshold: {type: 'number', label: 'LY Similarity Threshold', min: 0.0, max: 1.0, default: 0, path: '@relSearch.LYsimilarityTreshold',
                                              hover_tip: 'A number between 0 and 1 for the degree\nof similaity of LY Visually Similar Kanjis.',},
                       NiaiSimilarityTreshold: {type: 'number', label: 'Niai Similarity Threshold', min: 0.0, max: 1.0, default: 0, path: '@relSearch.NiaiSimilarityTreshold',
                                                hover_tip: 'A number between 0 and 1 for the degree\nof similaity of LY Visually Similar Kanjis.',},
                       NiaiAlternate: {type:'checkbox',label:'Use Niai Alternate Sources',hover_tip:'Add alternate sources to Niai visually similar\ndata to the main sources.\nThis will return more similar kanji.',
                                       path: '@relSearch.NiaiAlternate', default:0.3, min: 0.0, max: 1.0,
                                      },
                       divider: {type: 'divider'},
                       searchTerms: {type: 'html', label: 'Search terms', hover_tip: searchTermHovertip, path: '@relSearch.searchTerms',
                                     html: html,},
                       itemType: {type: 'list', multi: true, label: 'Item Type of Search Terms', path: '@relSearch.itemType',
                                  hover_tip: 'Further specify the search terms to be of the given types.',
                                  default: {radical: true, kanji: true, vocabulary: true, trad_rad: true},
                                  content: {radical: 'Radical', kanji: 'Kanji', vocabulary: 'Vocabulary', trad_rad: 'Traditional Radical'},},
                      },
        };

        let dialog = new wkof.Settings(dialogConfig);
        dialog.open();

        // ui issue: do not let the calling dialog be visible
        let originalDisplay = $originalDialog.css('display');
        $originalDialog.css('display', 'none');

        // work around some framework limitations regarding html types
        let $searchTerms = $('#'+areaId);
        $searchTerms.val(wkof.settings[settingsScriptId].relSearch.searchTerms);
        $searchTerms.change(textareaChanged);
        let $label = $searchTerms.closest('form').children('.left');
        $label.css('width', 'calc(100% - 5px)');
        $label.children('label').css('text-align', 'left');
        $label.attr('title', searchTermHovertip);

        function on_close(){
            $originalDialog.css('display', originalDisplay);
            if (typeof config.on_close === 'function') config.on_close();
        };

        function on_save(){
            set_value(path, get_value('wkof.settings.'+settingsScriptId+'.relSearch'));
            if (typeof config.on_save === 'function') config.on_save();
        };

        function on_cancel(){
            if (typeof config.on_cancel === 'function') config.on_cancel();
        };

        function textareaChanged(e){
            wkof.settings[settingsScriptId].relSearch.searchTerms = $searchTerms.val();
        };
    };

    function prepareRelatedSearch(filterValue){
        let returns = filterValue.returns;
        dataRequired.items = returns.components || returns.usedIn || returns.WKvisuallySimilar || returns.NiaiVisuallySimilar;
        dataRequired.larsYencken = returns.LYvisuallySimilar || (returns.NiaiVisuallySimilar && filterValue.NiaiAlternate);
        dataRequired.niai = returns.NiaiVisuallySimilar;
        dataRequired.keisei = (returns.phonetic || returns.nonPhonetic || returns.keiseiRelated);
        return loadMissingData().then(function(){prepareRelatedFilterValueMap(filterValue)});

        function prepareRelatedFilterValueMap(filterValue){
            let validItemTypes = filterValue.itemType;
            for (let searchTerm of filterValue.searchTermsArray){
                let idx = filterValue.searchData[searchTerm];
                idx.components = {};
                if (searchTerm in componentIndexKan && validItemTypes.kanji) idx.components.radical = componentIndexKan[searchTerm];
                if (searchTerm in componentIndexVoc && validItemTypes.vocabulary) idx.components.kanji = componentIndexVoc[searchTerm];
                idx.usedIn = {};
                if (searchTerm in usedInIndexRad && validItemTypes.radical) idx.usedIn.kanji = usedInIndexRad[searchTerm];
                if (searchTerm in usedInIndexKan && validItemTypes.kanji) idx.usedIn.vocabulary = usedInIndexKan[searchTerm];
                idx.WKvisuallySimilar = {};
                if (searchTerm in WKvisuallySimilarIndex && validItemTypes.kanji) idx.WKvisuallySimilar.kanji = WKvisuallySimilarIndex[searchTerm];
                idx.keiseiCompounds = {};
                if (returns.phonetic && searchTerm in keiseiCompoundIndex && (validItemTypes.kanji || validItemTypes.radical)){
                    idx.keiseiCompounds.radical = keiseiCompoundIndex[searchTerm];
                    idx.keiseiCompounds.kanji = keiseiCompoundIndex[searchTerm];
                };
                idx.keiseiNonCompounds = {};
                if (returns.nonPhonetic && searchTerm in keiseiNonCompoundIndex && (validItemTypes.kanji || validItemTypes.radical)){
                    idx.keiseiNonCompounds.radical = keiseiNonCompoundIndex[searchTerm];
                    idx.keiseiNonCompounds.kanji = keiseiNonCompoundIndex[searchTerm];
                };
                idx.keiseiRelated = {};
                if (returns.keiseiRelated && searchTerm in keiseiRelatedMarksIndex && (validItemTypes.kanji || validItemTypes.radical)){
                    idx.keiseiRelated.radical = keiseiRelatedMarksIndex[searchTerm];
                    idx.keiseiRelated.kanji = keiseiRelatedMarksIndex[searchTerm];
                };
                idx.LYvisuallySimilar = {};
                if (returns.LYvisuallySimilar && searchTerm in visuallySimilarData && validItemTypes.kanji){
                    idx.LYvisuallySimilar.kanji = [];
                    let threshold = filterValue.LYsimilarityTreshold;
                    for (let value of visuallySimilarData[searchTerm]){
                        if (value.score >= threshold && (value.kan in componentIndexKan)) idx.LYvisuallySimilar.kanji.push(value.kan);
                    };
                };
                idx.NiaiVisuallySimilar = {};
                if (returns.NiaiVisuallySimilar && validItemTypes.kanji){
                    idx.NiaiVisuallySimilar.kanji = makeKanjiListNiai(searchTerm);
                };
                idx = filterValue.searchData[searchTerm];
                idx.tradRadInKanji = {};
                if (searchTerm in tradRadInKanji && validItemTypes.kanji){
                    idx.tradRadInKanji.trad_rad = tradRadInKanji[searchTerm];
                };
                idx.kanjiwithTradRad = {};
                if (searchTerm in traditionalRadicals && validItemTypes.trad_rad){
                    idx.kanjiwithTradRad.kanji = traditionalRadicals[searchTerm].data.kanji;
                };
                if (searchTerm in kanjiwithTradRadbyName && validItemTypes.trad_rad){
                    idx.kanjiwithTradRad.kanji = kanjiwithTradRadbyName[searchTerm];
                };
            };
        };

        function makeKanjiListNiai(kanji){
            const sources = [
                {"id": "noto_db",        "base_score": 0.1},
                {"id": "keisei_db",      "base_score": 0.65},
            ];
            const alt_sources = [
                {"id": "old_script_db",  "base_score": 0.4},
                {"id": "yl_radical_db",  "base_score": -0.2},
                {"id": "stroke_dist_db", "base_score": -0.2},
            ];
            let selectedSources, score, old_score, similarData;
            let alternate = filterValue.NiaiAlternate;
            if (alternate){
                selectedSources = [...alt_sources, ...sources];
            } else {
                selectedSources = sources;
            };
            let similar_kanji = {};
            for (let source of selectedSources ){
                let threshold = filterValue.NiaiSimilarityTreshold;

                switch (source.id){
                    case 'noto_db': // has scores
                        if (!(kanji in wk_niai_noto_db)) continue;
                        similarData = wk_niai_noto_db[kanji];
                        break;
                    case 'keisei_db': // no scores
                        if (!(kanji in from_keisei_db)) continue;
                        similarData = from_keisei_db[kanji];
                        break;
                    case 'old_script_db': // no scores
                        if (!(kanji in old_script_db)) continue;
                        similarData = old_script_db[kanji];
                        break;
                    case 'yl_radical_db':; // has scores
                        if (!(kanji in yl_radical_db)) continue;
                        similarData = yl_radical_db[kanji];
                        break;
                    case 'stroke_dist_db': // has scrores
                        if (!(kanji in visuallySimilarData)) continue;
                        similarData = visuallySimilarData[kanji];
                        break;
                };

                switch (source.id){
                    case 'noto_db': // has scores
                    case 'yl_radical_db': // has scores
                    case 'stroke_dist_db': // has scrores
                        for (let similar of similarData){
                            score = similar.score + source.base_score;
                            old_score = (similar.kan in similar_kanji ? similar_kanji[similar.kan].score :  0.0);
                            if ((score > threshold || (score > 0.0 && old_score > 0.0)) && (typeof componentIndexKan[similar.kan]) === 'object'){
                                similar_kanji[similar.kan] = {kan: similar.kan, score: score};
                            } else if (score < 0) {
                                delete similar_kanji[similar.kan];
                            };
                        };
                        break;
                    case 'keisei_db': // no scores
                    case 'old_script_db': // no scores
                        score = source.base_score;
                        for (let similar of similarData){
                            //old_score = (similar in similar_kanji ? similar_kanji[similar].score :  0.0);
                            if ((typeof componentIndexKan[similar]) === 'object'){
                                similar_kanji[similar] = {kan: similar, score: score};
                            } else if (score < 0) {
                                delete similar_kanji[similar];
                            };
                        };
                        break;
                };
            };
            let acc = Object.values(similar_kanji);
            acc = acc.map((a) => a.kan)
            return acc;
        };

    };

    function prepareRelatedFilterValue(filterValueOriginal){
        // Don't modify the original because it is a pointer to the filter settings - modifying it will accumulate trash in the settings
        var filterValue = $.extend({}, filterValueOriginal);
        filterValue.searchTermsArray = split_list(filterValue.searchTerms);
        filterValue.searchData = {};
        for (let searchTerm of filterValue.searchTermsArray) filterValue.searchData[searchTerm] = {};
        return filterValue;
    };

    function reportRelatedResult(item, itemType, match, place){
        item.report = 'Relates to '+itemType+' '+match+' as '+place;
    };

    function relatedSearchFilter(filterValue, item) {
        if (item.data === undefined) {
            return false;
        };

        let compMapType = {radical: 'kanji', kanji: 'vocabulary'};
        let usedMapType = {vocabulary: 'kanji', kanji: 'radical'};
        let itemType = item.object;
        let returns = filterValue.returns;
        let charKey = (item.data.characters !== null ? item.data.characters : item.data.slug);

        for (var searchTerm of filterValue.searchTermsArray) {
            let result = false;
            let searchData = filterValue.searchData[searchTerm];

            if (returns.matchedItem){
                if (filterValue.itemType[itemType]) {
                    if (item.data.characters !== null && item.data.characters === searchTerm) {
                        reportRelatedResult(item, itemType, searchTerm, 'being the item.');
                        return true;
                    };
                    if (itemType === 'radical' && item.data.slug === searchTerm.replace(/ /g, '-')) {
                        reportRelatedResult(item, itemType, searchTerm, 'being the item.');
                        return true;
                    };
                    if (itemType === 'trad_rad') {
                        for (let meaningData of item.data.meanings){
                            if (meaningData.meaning === searchTerm){
                                reportRelatedResult(item, itemType, searchTerm, 'being the item.');
                                return true;
                            };
                        };
                    };
                };
            };

            if (returns.components) {
                if (itemType in searchData.components && searchData.components[itemType].indexOf(item.id) >= 0) {
                    reportRelatedResult(item, compMapType[itemType], searchTerm, 'a component.');
                    return true;
                };
            };

            if (returns.WKvisuallySimilar) {
                if (itemType in searchData.WKvisuallySimilar && searchData.WKvisuallySimilar[itemType].indexOf(item.id) >= 0) {
                    reportRelatedResult(item, itemType, searchTerm, 'WK visually similar.');
                    return true;
                };
            };

            if (returns.usedIn) {
                if (itemType in searchData.usedIn && searchData.usedIn[itemType].indexOf(item.id) >= 0) {
                    reportRelatedResult(item, usedMapType[itemType], searchTerm, 'an item where it is used.');
                    return true;
                };
            };

            if (returns.phonetic) {
                if (itemType in searchData.keiseiCompounds && searchData.keiseiCompounds[itemType].indexOf(charKey) >= 0) {
                    reportRelatedResult(item, itemType, searchTerm, 'a keisei phonetic compound.');
                    return true;
                };
            };

            if (returns.nonPhonetic) {
                if (itemType in searchData.keiseiNonCompounds && searchData.keiseiNonCompounds[itemType].indexOf(charKey) >= 0) {
                    reportRelatedResult(item, itemType, searchTerm, 'a non phonetic compound.');
                    return true;
                };
            };

            if (returns.keiseiRelated) {
                if (itemType in searchData.keiseiRelated && searchData.keiseiRelated[itemType].indexOf(charKey) >= 0) {
                    reportRelatedResult(item, itemType, searchTerm, 'a related phonetic mark.');
                    return true;
                };
            };

            if (returns.LYvisuallySimilar) {
                if (itemType in searchData.LYvisuallySimilar && searchData.LYvisuallySimilar[itemType].indexOf(charKey) >= 0) {
                    reportRelatedResult(item, itemType, searchTerm, 'a visually similar kanji according to Lars Yencken.');
                    return true;
                };
            };

            if (returns.NiaiVisuallySimilar) {
                if (itemType in searchData.NiaiVisuallySimilar && searchData.NiaiVisuallySimilar[itemType].indexOf(charKey) >= 0) {
                    reportRelatedResult(item, itemType, searchTerm, 'a visually similar kanji according to Niai.');
                    return true;
                };
            };

            if (returns.tradRadInKanji) {
                if (itemType in searchData.tradRadInKanji && searchData.tradRadInKanji[itemType].indexOf(charKey) >= 0) {
                    reportRelatedResult(item, 'kanji', searchTerm, 'a component of the kanji.');
                    return true;
                };
            };

            if (returns.kanjiwithTradRad) {
                if (itemType in searchData.kanjiwithTradRad && searchData.kanjiwithTradRad[itemType].indexOf(charKey) >= 0) {
                    reportRelatedResult(item, 'traditional radical', searchTerm, 'a kanji where it is used.'); return true;
                };
            };

        };

        return false;
    };

    // END Related Search

    // BEGIN Explicit List
    let explicitListHover_tip = 'Enter explicit list of items.';

    function registerExplicitListFilter() {
        const registration = {
            type: 'button',
            label: 'Explicit List',
            default: explicitList_defaults,
            callable_dialog: true,
            on_click: explicitListDialog,
            filter_value_map: prepareValueExplicitList,
            filter_func: explicitListFilter,
            set_options: function(options) { options.subjects = true; },
            hover_tip: explicitListHover_tip,
        };

        wkof.ItemData.registry.sources.wk_items.filters[explicitListFilterName] = $.extend(true, {}, registration);
        wkof.ItemData.registry.sources.wk_items.filters[explicitListFilterName].alternate_sources = ['trad_rad'];
        wkof.ItemData.registry.sources.trad_rad.filters[explicitListFilterName] = $.extend(true, {}, registration);
        wkof.ItemData.registry.sources.trad_rad.filters[explicitListFilterName].main_source = 'wk_items';

    };

    let explicitList_defaults = {explicitList_radical: '',
                                 explicitList_kanji: '',
                                 explicitList_vocabulary: '',
                                 explicitList_kana_vocabulary: '',
                                 explicitList_trad_rad: '',
                      };

    function explicitListDialog(name, config, on_change){
        let $originalDialog = $('#wkof_ds').find('[role="dialog"]');
        let scriptId = config.script_id || $originalDialog.attr("aria-describedby").slice(6);
        let path;
        if (config.path){
            path = config.path.replaceAll('@', 'wkof.settings["'+scriptId+'"].');
        } else if (scriptId === 'ss_quiz') {

            // Self Study Quiz doesn't define the path but we know what it is provided we find the source
            let row = $(this.delegateTarget);
            let panel = row.closest('[role="tabpanel"]');
            let source = panel.attr('id').match(/^ss_quiz_pg_(.*)$/)[1];
            path = 'wkof.settings.ss_quiz.ipresets[wkof.settings.ss_quiz.active_ipreset].content.'+source+'.filters.advSearchFilters_explicitList.value'

            // initialize in case it is not already initialized
            let v = $.extend(true, {}, get_value(path));
            set_value(path, v)
        } else {
            throw 'config.path is not defined';
        };

        let value = $.extend(true, {}, explicitList_defaults, get_value(path));
        set_value(path, value);
        wkof.settings[settingsScriptId] = wkof.settings[settingsScriptId] || {};
        wkof.settings[settingsScriptId].explicitList = $.extend(true, {}, explicitList_defaults, get_value(path));

        let areaIdRadical = settingsScriptId+'_radical';
        let html_radical = '<textarea id="'+areaIdRadical+'" rows="2" style="width: calc(100% - 5px)"></textarea>';
        let areaIdKanji = settingsScriptId+'_kanji';
        let html_kanji = '<textarea id="'+areaIdKanji+'" rows="2" style="width: calc(100% - 5px)"></textarea>';
        let areaIdVocabulary = settingsScriptId+'_vocabulary';
        let html_vocabulary = '<textarea id="'+areaIdVocabulary+'" rows="2" style="width: calc(100% - 5px)"></textarea>';
        let areaIdKana_Vocabulary = settingsScriptId+'_kana_vocabulary';
        let html_kana_vocabulary = '<textarea id="'+areaIdKana_Vocabulary+'" rows="2" style="width: calc(100% - 5px)"></textarea>';
        let areaIdTrad_rad = settingsScriptId+'_trad_rad';
        let html_trad_rad = '<textarea id="'+areaIdTrad_rad+'" rows="2" style="width: calc(100% - 5px)"></textarea>';
        let downloadText = '<button><a download="Filter Item List.txt" id="'+settingsScriptId+'_itemList_link" style="text-decoration:none;color:#000000;">Download Configured Items</a></button>';
        let inputFile = '<input id="'+settingsScriptId+'_inputFile" type="file" title="Select a file for uploading items with the button above.">';

        let dialogConfig = {
            script_id: settingsScriptId,
            title: 'Explicit List',
            on_save: on_save,
            on_cancel: on_cancel,
            on_close: on_close,
            no_bkgd: true,
            settings: {explicitList_radical:{type: "html", html: html_radical, label: 'Radicals',},
                       explicitList_kanji:{type: "html", html: html_kanji, label: 'Kanji',},
                       explicitList_vocabulary:{type: "html", html: html_vocabulary, label: 'Kanji Vocabulary',},
                       explicitList_kana_vocabulary:{type: "html", html: html_kana_vocabulary, label: 'Kana Only Vocabulary',},
                       explicitList_trad_rad:{type: "html", html: html_trad_rad, label: 'Traditional Radicals',},
                       explicitList_divider:{type: 'divider'},
                       explicitList_download:{type: "html", label: ' ', html: downloadText,},
                       explicitList_divider2:{type: 'divider'},
                       explicitList_upload:{type: "button", label: 'Set Items From Selected File', on_click: uploadFile,
                                            hover_tip: 'Upload your items from a previously downloaded file.',},
                       explicitList_input:{type: 'html', html: inputFile,},
                      },
        };

        let dialog = new wkof.Settings(dialogConfig);
        dialog.open();

        // ui issue: do not let the calling dialog be visible
        let originalDisplay = $originalDialog.css('display');
        $originalDialog.css('display', 'none');

        // work around some framework limitations regarding html types
        let $explicitList_radical = $('#'+areaIdRadical);
        $explicitList_radical.val(wkof.settings[settingsScriptId].explicitList.explicitList_radical);
        $explicitList_radical.change(radicalChanged);
        let $label = $explicitList_radical.prev();
        $label.css('width', 'calc(100% - 5px)');
        $label.children('label').css('text-align', 'left');
        let radicalHovertip = 'List your items separated by commas\nYour search terms must match the characters for the item exactly.\nEnglish name of radicals are accepted.';
        $label.attr('title', radicalHovertip);

        let $explicitList_kanji = $('#'+areaIdKanji);
        $explicitList_kanji.val(wkof.settings[settingsScriptId].explicitList.explicitList_kanji);
        $explicitList_kanji.change(kanjiChanged);
        $label = $explicitList_kanji.prev();
        $label.css('width', 'calc(100% - 5px)');
        $label.children('label').css('text-align', 'left');
        let itemlHovertip = 'List your items separated by commas.\nYour search terms must match the characters for the item exactly.';
        $label.attr('title', itemlHovertip);

        let $explicitList_vocabulary = $('#'+areaIdVocabulary);
        $explicitList_vocabulary.val(wkof.settings[settingsScriptId].explicitList.explicitList_vocabulary);
        $explicitList_vocabulary.change(vocabularyChanged);
        $label = $explicitList_vocabulary.prev();
        $label.css('width', 'calc(100% - 5px)');
        $label.children('label').css('text-align', 'left');
        $label.attr('title', itemlHovertip);

        let $explicitList_kana_vocabulary = $('#'+areaIdKana_Vocabulary);
        $explicitList_kana_vocabulary.val(wkof.settings[settingsScriptId].explicitList.explicitList_kana_vocabulary);
        $explicitList_kana_vocabulary.change(kana_vocabularyChanged);
        $label = $explicitList_kana_vocabulary.prev();
        $label.css('width', 'calc(100% - 5px)');
        $label.children('label').css('text-align', 'left');
        $label.attr('title', itemlHovertip);

        let $explicitList_trad_rad = $('#'+areaIdTrad_rad);
        $explicitList_trad_rad.val(wkof.settings[settingsScriptId].explicitList.explicitList_trad_rad);
        $explicitList_trad_rad.change(trad_radChanged);
        $label = $explicitList_trad_rad.prev();
        $label.css('width', 'calc(100% - 5px)');
        $label.children('label').css('text-align', 'left');
        $label.attr('title', radicalHovertip);

        let $download = $('#advSearchFilters_itemList_link');
        $download.attr('title', 'Download Your Configured Items In a File');
        setDownloadLink();

        function on_close(){
            $originalDialog.css('display', originalDisplay);
            if (typeof config.on_close === 'function') config.on_close();
        };

        function on_save(){
            set_value(path, get_value('wkof.settings.'+settingsScriptId+'.explicitList'));
            if (typeof config.on_save === 'function') config.on_save();
        };

        function on_cancel(){
            if (typeof config.on_cancel === 'function') config.on_cancel();
        };

        function radicalChanged(e){
            wkof.settings[settingsScriptId].explicitList.explicitList_radical = $explicitList_radical.val();
            setDownloadLink()
        };

        function kanjiChanged(e){
            wkof.settings[settingsScriptId].explicitList.explicitList_kanji = $explicitList_kanji.val();
            setDownloadLink()
        };

        function vocabularyChanged(e){
            wkof.settings[settingsScriptId].explicitList.explicitList_vocabulary = $explicitList_vocabulary.val();
            setDownloadLink()
        };

        function kana_vocabularyChanged(e){
            wkof.settings[settingsScriptId].explicitList.explicitList_kana_vocabulary = $explicitList_kana_vocabulary.val();
            setDownloadLink()
        };

        function trad_radChanged(e){
            wkof.settings[settingsScriptId].explicitList.explicitList_trad_rad = $explicitList_trad_rad.val();
            setDownloadLink()
        };
    };

    //function setDownloadLink(name, value, config){
    function setDownloadLink(){
        let radicalElem = $('#'+settingsScriptId+'_radical');
        let kanjiElem = $('#'+settingsScriptId+'_kanji');
        let vocabularyElem = $('#'+settingsScriptId+'_vocabulary');
        let kana_vocabularyElem = $('#'+settingsScriptId+'_kana_vocabulary');
        let trad_radElem = $('#'+settingsScriptId+'_trad_rad');
        let radicals = radicalElem.val();
        let kanji = kanjiElem.val();
        let vocabulary = vocabularyElem.val();
        let kana_vocabulary = kana_vocabularyElem.val();
        let trad_rad = trad_radElem.val();
        let encoded = makeEncode(radicals, kanji, vocabulary, kana_vocabulary, trad_rad);
        let downloadElem = $('#'+settingsScriptId+'_itemList_link');
        downloadElem.attr("href", "data:text/plain; charset=utf-8,"+encoded);
    };

    function makeEncode(radicals, kanji, vocabulary, kana_vocabulary, trad_rad){
        let list = [];
        list.push('radicals');
        list.push(radicals);
        list.push('kanji');
        list.push(kanji);
        list.push('vocabulary');
        list.push(vocabulary);
        list.push('kana_vocabulary');
        list.push(kana_vocabulary);
        list.push('traditional radicals');
        list.push(trad_rad);
        let text = list.join('\n');
        return encodeURI("\uFEFF"+text);
    };

    function uploadFile(name, config, on_change){
        let buttons = $(this.target).closest('.right');
        buttons.find('.note').remove();
        let fileElem = $("#advSearchFilters_inputFile");
        let filenames = fileElem.prop('files');
        if (filenames.length === 0){
            buttons.append('<div class="note error">Plese select a file</div>');
            return;
        };
        let filename = filenames[0];
        let reader = new FileReader();
        reader.onload = validateReception;
        reader.readAsText(filename);

        function validateReception(event){
            let result = receiveText(event);
            if (typeof result === 'string'){
                buttons.find('.note').remove();
                buttons.append('<div class="note error">'+result+'</div>');
            };
        };

        function receiveText(event){
            let text = event.target.result;
            let radicals, kanji, vocabulary, kana_vocabulary, trad_rad;
            let errorMsg = 'Invalid file content';
            text = text.replaceAll('\n','');

            let start = text.indexOf('radicals');
            if (start !== 0) return errorMsg;
            start = start + 'radicals'.length;
            let end = text.indexOf('kanji');
            if (end < start) return errorMsg+' radicals';
            radicals = text.slice(start, end);

            start = end + 'kanji'.length;
            end = text.indexOf('vocabulary');
            if (end < start) return errorMsg+' kanji';
            kanji = text.slice(start, end);

            start = end + 'vocabulary'.length;
            // to support old versions from befroe the introduction of the kana_vocabulary object type
            end =  text.indexOf('kana_vocabulary') === -1 ? text.indexOf('traditional radicals') : text.indexOf('kana_vocabulary');
            if (end < start) return errorMsg+' vocabulary';
            vocabulary = text.slice(start, end);

            start = end + 'kana_vocabulary'.length;
            end = text.indexOf('traditional radicals');
            // to support old versions from befroe the introduction of the kana_vocabulary object type
            if (start < end) {kana_vocabulary = text.slice(start, end)} else {kana_vocabulary = ''};

            start = end + 'traditional radicals'.length;
            trad_rad = text.slice(start);

            let elem = $('#'+settingsScriptId+'_radical');
            elem.val(radicals);
            elem.change();
            elem = $('#'+settingsScriptId+'_kanji');
            elem.val(kanji);
            elem.change();
            elem = $('#'+settingsScriptId+'_vocabulary');
            elem.val(vocabulary);
            elem.change();
            elem = $('#'+settingsScriptId+'_kana_vocabulary');
            elem.val(kana_vocabulary);
            elem.change();
            elem = $('#'+settingsScriptId+'_trad_rad');
            elem.val(trad_rad);
            elem.change();
            return true;
        };
    };

    function prepareValueExplicitList(filterValue){
        let renamed = {};
        renamed.radical = split_list(filterValue.explicitList_radical).map((str) => str.replace(/ /g, '-'));
        renamed.kanji = split_list(filterValue.explicitList_kanji);
        renamed.vocabulary = split_list(filterValue.explicitList_vocabulary);
        renamed.kana_vocabulary = split_list(filterValue.explicitList_kana_vocabulary);
        renamed.trad_rad = split_list(filterValue.explicitList_trad_rad);
        return renamed;
    };

    function explicitListFilter(filterValue, item) {
        let type = item.object;
        if (type === 'radical') if (filterValue.radical.indexOf(item.data.slug.toLowerCase()) >= 0) return true;
        if (type === 'radical') if (item.data.characters === null) return false;
        if (type === 'trad_rad'){
            for (let mm of item.data.meanings){
                if (filterValue.trad_rad.indexOf(mm.meaning.toLowerCase()) >= 0) return true;
            };
        };
        return filterValue[type].indexOf(item.data.characters) >= 0;
    };

    // END Explicit List

    // BEGIN Explicit Block
    let explicitBlockHover_tip = 'Enter explicit list of items to be barred from displaying.';

    function registerExplicitBlockFilter() {
        const registration = {
            type: 'button',
            label: 'Explicit Block',
            default: explicitBlock_defaults,
            callable_dialog: true,
            on_click: explicitBlockDialog,
            filter_value_map: prepareValueExplicitBlock,
            filter_func: explicitBlockFilter,
            set_options: function(options) { options.subjects = true; },
            hover_tip: explicitBlockHover_tip,
        };

        wkof.ItemData.registry.sources.wk_items.filters[explicitBlockFilterName] = $.extend(true, {}, registration);
        wkof.ItemData.registry.sources.wk_items.filters[explicitBlockFilterName].alternate_sources = ['trad_rad'];
        wkof.ItemData.registry.sources.trad_rad.filters[explicitBlockFilterName] = $.extend(true, {}, registration);
        wkof.ItemData.registry.sources.trad_rad.filters[explicitBlockFilterName].main_source = 'wk_items';

    };

    let explicitBlock_defaults = {explicitBlock_radical: '',
                                  explicitBlock_kanji: '',
                                  explicitBlock_vocabulary: '',
                                  explicitBlock_kana_vocabulary: '',
                                  explicitBlock_trad_rad: '',
                                 };

    function explicitBlockDialog(name, config, on_change){
        let $originalDialog = $('#wkof_ds').find('[role="dialog"]');
        let scriptId = config.script_id || $originalDialog.attr("aria-describedby").slice(6);
        let path;
        if (config.path){
            path = config.path.replaceAll('@', 'wkof.settings["'+scriptId+'"].');
        } else if (scriptId === 'ss_quiz') {

            // Self Study Quiz doesn't define the path but we know what it is provided we find the source
            let row = $(this.delegateTarget);
            let panel = row.closest('[role="tabpanel"]');
            let source = panel.attr('id').match(/^ss_quiz_pg_(.*)$/)[1];
            path = 'wkof.settings.ss_quiz.ipresets[wkof.settings.ss_quiz.active_ipreset].content.'+source+'.filters.advSearchFilters_explicitBlock.value'

            // initialize in case it is not already initialized
            let v = $.extend(true, {}, get_value(path));
            set_value(path, v)
        } else {
            throw 'config.path is not defined';
        };

        let value = $.extend(true, {}, explicitBlock_defaults, get_value(path));
        set_value(path, value);
        wkof.settings[settingsScriptId] = wkof.settings[settingsScriptId] || {};
        wkof.settings[settingsScriptId].explicitBlock = $.extend(true, {}, explicitBlock_defaults, get_value(path));

        // The width styling attribute must be in-line in textareas otherwise the dialog box won't be positionned properly
        // at the center of the screen. In that case the save button will be outsie the visible area forcing the user to
        // manually replace the dialog to access this button.
        let areaIdRadical = settingsScriptId+'_radical';
        let html_radical = '<textarea id="'+areaIdRadical+'" rows="2" style="width: calc(100% - 5px)"></textarea>';
        let areaIdKanji = settingsScriptId+'_kanji';
        let html_kanji = '<textarea id="'+areaIdKanji+'" rows="2" style="width: calc(100% - 5px)"></textarea>';
        let areaIdVocabulary = settingsScriptId+'_vocabulary';
        let html_vocabulary = '<textarea id="'+areaIdVocabulary+'" rows="2" style="width: calc(100% - 5px)"></textarea>';
        let areaIdKana_Vocabulary = settingsScriptId+'_kana_vocabulary';
        let html_kana_vocabulary = '<textarea id="'+areaIdKana_Vocabulary+'" rows="2" style="width: calc(100% - 5px)"></textarea>';
        let areaIdTrad_rad = settingsScriptId+'_trad_rad';
        let html_trad_rad = '<textarea id="'+areaIdTrad_rad+'" rows="2" style="width: calc(100% - 5px)"></textarea>';
        let downloadText = '<button><a download="Filter Item List.txt" id="'+settingsScriptId+'_itemList_link" style="text-decoration:none;color:#000000;">Download Configured Items</a></button>';
        let inputFile = '<input id="'+settingsScriptId+'_inputFile" type="file" title="Select a file for uploading items with the button above.">';

        let dialogConfig = {
            script_id: settingsScriptId,
            title: 'Explicit List',
            on_save: on_save,
            on_cancel: on_cancel,
            on_close: on_close,
            no_bkgd: true,
            settings: {explicitBlock_radical:{type: "html", html: html_radical, label: 'Radicals',},
                       explicitBlock_kanji:{type: "html", html: html_kanji, label: 'Kanji',},
                       explicitBlock_vocabulary:{type: "html", html: html_vocabulary, label: 'Vocabulary',},
                       explicitBlock_kana_vocabulary:{type: "html", html: html_kana_vocabulary, label: 'Kana Vocabulary',},
                       explicitBlock_trad_rad:{type: "html", html: html_trad_rad, label: 'Traditional Radicals',},
                       explicitBlock_divider:{type: 'divider'},
                       explicitBlock_download:{type: "html", label: ' ', html: downloadText,},
                       explicitBlock_divider2:{type: 'divider'},
                       explicitBlock_upload:{type: "button", label: 'Set Items From Selected File', on_click: uploadFileBlock,
                                                 hover_tip: 'Upload your items from a previously downloaded file.',},
                       explicitBlock_input:{type: 'html', html: inputFile,},
                      },
        };

        let dialog = new wkof.Settings(dialogConfig);
        dialog.open();

        // ui issue: do not let the calling dialog be visible
        let originalDisplay = $originalDialog.css('display');
        $originalDialog.css('display', 'none');

        // work around some framework limitations regarding html types
        let $explicitBlock_radical = $('#'+areaIdRadical);
        $explicitBlock_radical.val(wkof.settings[settingsScriptId].explicitBlock.explicitBlock_radical);
        $explicitBlock_radical.change(radicalChanged);
        let $label = $explicitBlock_radical.prev();
        $label.css('width', 'calc(100% - 5px)');
        $label.children('label').css('text-align', 'left');
        let radicalHovertip = 'List your items separated by commas\nYour search terms must match the characters for the item exactly.\nEnglish name of radicals are accepted.';
        $label.attr('title', radicalHovertip);

        let $explicitBlock_kanji = $('#'+areaIdKanji);
        $explicitBlock_kanji.val(wkof.settings[settingsScriptId].explicitBlock.explicitBlock_kanji);
        $explicitBlock_kanji.change(kanjiChanged);
        $label = $explicitBlock_kanji.prev();
        $label.css('width', 'calc(100% - 5px)');
        $label.children('label').css('text-align', 'left');
        let itemlHovertip = 'List your items separated by commas.\nYour search terms must match the characters for the item exactly.';
        $label.attr('title', itemlHovertip);

        let $explicitBlock_vocabulary = $('#'+areaIdVocabulary);
        $explicitBlock_vocabulary.val(wkof.settings[settingsScriptId].explicitBlock.explicitBlock_vocabulary);
        $explicitBlock_vocabulary.change(vocabularyChanged);
        $label = $explicitBlock_vocabulary.prev();
        $label.css('width', 'calc(100% - 5px)');
        $label.children('label').css('text-align', 'left');
        $label.attr('title', itemlHovertip);

        let $explicitBlock_kana_vocabulary = $('#'+areaIdKana_Vocabulary);
        $explicitBlock_kana_vocabulary.val(wkof.settings[settingsScriptId].explicitBlock.explicitBlock_kana_vocabulary);
        $explicitBlock_kana_vocabulary.change(kana_vocabularyChanged);
        $label = $explicitBlock_kana_vocabulary.prev();
        $label.css('width', 'calc(100% - 5px)');
        $label.children('label').css('text-align', 'left');
        $label.attr('title', itemlHovertip);

        let $explicitBlock_trad_rad = $('#'+areaIdTrad_rad);
        $explicitBlock_trad_rad.val(wkof.settings[settingsScriptId].explicitBlock.explicitBlock_trad_rad);
        $explicitBlock_trad_rad.change(trad_radChanged);
        $label = $explicitBlock_trad_rad.prev();
        $label.css('width', 'calc(100% - 5px)');
        $label.children('label').css('text-align', 'left');
        $label.attr('title', radicalHovertip);

        let $download = $('#advSearchFilters_itemList_linkBlock');
        $download.attr('title', 'Download Your Configured Items In a File');
        setDownloadLinkBlock();

        function on_close(){
            $originalDialog.css('display', originalDisplay);
            if (typeof config.on_close === 'function') config.on_close();
        };

        function on_save(){
            set_value(path, get_value('wkof.settings.'+settingsScriptId+'.explicitBlock'));
            if (typeof config.on_save === 'function') config.on_save();
        };

        function on_cancel(){
            if (typeof config.on_cancel === 'function') config.on_cancel();
        };

        function radicalChanged(e){
            wkof.settings[settingsScriptId].explicitBlock.explicitBlock_radical = $explicitBlock_radical.val();
            setDownloadLinkBlock()
        };

        function kanjiChanged(e){
            wkof.settings[settingsScriptId].explicitBlock.explicitBlock_kanji = $explicitBlock_kanji.val();
            setDownloadLinkBlock()
        };

        function vocabularyChanged(e){
            wkof.settings[settingsScriptId].explicitBlock.explicitBlock_vocabulary = $explicitBlock_vocabulary.val();
            setDownloadLinkBlock()
        };

        function kana_vocabularyChanged(e){
            wkof.settings[settingsScriptId].explicitBlock.explicitBlock_kana_vocabulary = $explicitBlock_kana_vocabulary.val();
            setDownloadLinkBlock()
        };

        function trad_radChanged(e){
            wkof.settings[settingsScriptId].explicitBlock.explicitBlock_trad_rad = $explicitBlock_trad_rad.val();
            setDownloadLinkBlock()
        };
    };

    function setDownloadLinkBlock(){
        let radicalElem = $('#'+settingsScriptId+'_radicalBlock');
        let kanjiElem = $('#'+settingsScriptId+'_kanjiBlock');
        let vocabularyElem = $('#'+settingsScriptId+'_vocabularyBlock');
        let kana_vocabularyElem = $('#'+settingsScriptId+'_kana_vocabularyBlock');
        let trad_radElem = $('#'+settingsScriptId+'_trad_radBlock');
        let radicals = radicalElem.val();
        let kanji = kanjiElem.val();
        let vocabulary = vocabularyElem.val();
        let kana_vocabulary = kana_vocabularyElem.val();
        let trad_rad = trad_radElem.val();
        let encoded = makeEncode(radicals, kanji, vocabulary, kana_vocabulary, trad_rad);
        let downloadElem = $('#'+settingsScriptId+'_itemList_linkBlock');
        downloadElem.attr("href", "data:text/plain; charset=utf-8,"+encoded);
    };

    function uploadFileBlock(name, config, on_change){
        let buttons = $(this.target).closest('.right');
        buttons.find('.note').remove();
        let fileElem = $("#advSearchFilters_inputFileBlock");
        let filenames = fileElem.prop('files');
        if (filenames.length === 0){
            buttons.append('<div class="note error">Plese select a file</div>');
            return;
        };
        let filename = filenames[0];
        let reader = new FileReader();
        reader.onload = validateReception;
        reader.readAsText(filename);

        function validateReception(event){
            let result = receiveText(event);
            if (typeof result === 'string'){
                buttons.find('.note').remove();
                buttons.append('<div class="note error">'+result+'</div>');
            };
        };

        function receiveText(event){
            let text = event.target.result;
            let radicals, kanji, vocabulary, kana_vocabulary, trad_rad;
            let errorMsg = 'Invalid file content';
            text = text.replaceAll('\n','');

            let start = text.indexOf('radicals');
            if (start !== 0) return errorMsg;
            start = start + 'radicals'.length;
            let end = text.indexOf('kanji');
            if (end < start) return errorMsg+' radicals';
            radicals = text.slice(start, end);

            start = end + 'kanji'.length;
            end = text.indexOf('vocabulary');
            if (end < start) return errorMsg+' kanji';
            kanji = text.slice(start, end);

            start = end + 'vocabulary'.length;
            // to support old versions from befroe the introduction of the kana_vocabulary object type
            end = (text.indexOf('kana_vocabulary') === -1) ? (text.indexOf('traditional radicals')) : (text.indexOf('kana_vocabulary'));
            if (end < start) return errorMsg+' vocabulary';
            vocabulary = text.slice(start, end);

            start = end + 'kana_vocabulary'.length;
            end = text.indexOf('traditional radicals');
            // to support old versions from befroe the introduction of the kana_vocabulary object type
            if (start < end) kana_vocabulary = text.slice(start, end);

            start = end + 'traditional radicals'.length;
            trad_rad = text.slice(start);

            let elem = $('#'+settingsScriptId+'_radicalBlock');
            elem.val(radicals);
            elem.change();
            elem = $('#'+settingsScriptId+'_kanjiBlock');
            elem.val(kanji);
            elem.change();
            elem = $('#'+settingsScriptId+'_vocabularyBlock');
            elem.val(vocabulary);
            elem.change();
            elem = $('#'+settingsScriptId+'_kana_vocabularyBlock');
            elem.val(kana_vocabulary);
            elem.change();
            elem = $('#'+settingsScriptId+'_trad_radBlock');
            elem.val(trad_rad);
            elem.change();
            return true;
        };
    };

    function prepareValueExplicitBlock(filterValue){
        let renamed = {};
        renamed.radical = split_list(filterValue.explicitBlock_radical).map((str) => str.replace(/ /g, '-'));
        renamed.kanji = split_list(filterValue.explicitBlock_kanji);
        renamed.vocabulary = split_list(filterValue.explicitBlock_vocabulary);
        renamed.kana_vocabulary = split_list(filterValue.explicitBlock_kana_vocabulary);
        renamed.trad_rad = split_list(filterValue.explicitBlock_trad_rad);
        return renamed;
    };

    function explicitBlockFilter(filterValue, item) {
        let type = item.object;
        if (type === 'radical') if (filterValue.radical.indexOf(item.data.slug.toLowerCase()) >= 0) return false;
        if (type === 'radical') if (item.data.characters === null) return true;
        if (type === 'trad_rad'){
            for (let mm of item.data.meanings){
                if (filterValue.trad_rad.indexOf(mm.meaning.toLowerCase()) >= 0) return false;
            };
        };
        return filterValue[type].indexOf(item.data.characters) < 0;
    };

    // END Explicit Block

    // BEGIN Keisei Semantic-Phonetic Composition

    // Source @acm2010 at the Keisei Semantic Phonetic composition script
    // https://community.wanikani.com/t/userscript-keisei-%E5%BD%A2%E5%A3%B0-semantic-phonetic-composition/21479

    //================================================
    // BEGIN code by @acm2010 released under GPL V3 or later license
    //================================================

    // Wrapper object for Keisei database interactions
    // #############################################################################
    function KeiseiDB()
    {
        this.KTypeEnum = Object.freeze({
            unknown:         0,
            hieroglyph:      1, // 象形: type of character representing pictures
            indicative:      2, // 指事: indicative (kanji whose shape is based on logical representation of an abstract idea)
            comp_indicative: 3, // 会意: kanji made up of meaningful parts (e.g. "mountain pass" is up + down + mountain)
            comp_phonetic:   4, // 形声: kanji in which one element suggests the meaning, the other the pronunciation
            derivative:      5, // 転注: applying an extended meaning to a kanji
            rebus:           6, // 仮借: borrowing a kanji with the same pronunciation to convey an unrelated term
            kokuji:          7, // kanji originating from Japan
            shinjitai:       8, // Character was simplified from a different form (seiji)
            unprocessed:     9  // not yet visited
        }
                                      );

        this.wkradical_to_phon = {};
    }
    // #############################################################################

    // #############################################################################
    (function() {
        "use strict";

        KeiseiDB.prototype = {
            constructor: KeiseiDB,

            // #####################################################################
            init: function()
            {
                this.genWKRadicalToPhon();
                this.mapPhoneticsToKan(); // added by prouleau for Item Inspector
                this.findSemanticForKan(); // added by prouleau for Item Inspector
            },
            // #####################################################################

            // #####################################################################
            kdata: function(kanji)
            {
                return this.kanji_db[kanji];
            },
            // #####################################################################

            // #####################################################################
            checkKanji: function(kanji)
            {
                return Boolean(this.kanji_db && (kanji in this.kanji_db));
            },
            // #####################################################################
            // #####################################################################
            checkPhonetic: function(phon)
            {
                return Boolean(this.phonetic_db && (phon in this.phonetic_db));
            },
            // #####################################################################
            // #####################################################################
            checkRadical: function(phon)
            {
                return Boolean(this.phonetic_db && this.phonetic_db[phon] && this.phonetic_db[phon][`wk-radical`]);
            },
            // #####################################################################
            // #####################################################################
            getKType: function(kanji)
            {
                return this.KTypeEnum[this.kanji_db[kanji].type];
            },
            // #####################################################################

            // #####################################################################
            getKItem: function(kanji)
            {
                if (kanji in this.kanji_db)
                    return this.kanji_db[kanji];
                else
                    return {};
            },
            // #####################################################################

            // #####################################################################
            getWKItem: function(kanji)
            {
                if (kanji in this.wk_kanji_db)
                    return this.wk_kanji_db[kanji];
                else
                    return {"level": "N/A"};
            },
            // #####################################################################

            // #####################################################################
            getKReadings: function(kanji)
            {
                let result = [];

                if (!(kanji in this.kanji_db))
                    result = this.phonetic_db[kanji].readings;
                else
                    result = this.kanji_db[kanji].readings;

                if (!result.length)
                    return this.getWKOnyomi(kanji);
                else
                    return result;
            },
            // #####################################################################
            // #####################################################################
            getKPhonetic: function(kanji)
            {
                if (kanji in this.kanji_db)
                    return this.kanji_db[kanji].phonetic;
                else
                    return null;
            },
            // #####################################################################

            // Phonetic DB functions

            // #####################################################################
            getPCompounds: function(phon)
            {
                if (phon in this.phonetic_db)
                    return this.phonetic_db[phon].compounds;
                else
                {
                    console.log(`The following phonetic is not in the DB!`, phon);
                    return [];
                }
            },
            // #####################################################################
            // added by prouleau for Item Inspector
            // The phonetic field is used by @acm2010 code but it seems is not initialized
            // anywhere in the code. I had to reverse engineer the initialization
            // #####################################################################
            mapPhoneticsToKan: function()
            {
                Object.keys(this.kanji_db).forEach(
                    (kan) => {
                        this.kanji_db[kan].phonetic = [];
                    });
                Object.keys(this.phonetic_db).forEach(
                    (phon) => {
                        this.phonetic_db[phon].compounds.forEach(
                            (kan) => {
                                if (this.kanji_db[kan].phonetic.indexOf(phon) === -1 && this.checkPhonetic(phon)) this.kanji_db[kan].phonetic.push(phon);
                            })
                    })
            },
            // #####################################################################
            // added by prouleau for Item Inspector
            // The semantic field is used by @acm2010 code but it seems is not initialized
            // anywhere in the code. I had to reverse engineer the initialization
            // #####################################################################
            findSemanticForKan: function()
            {
                /* Object.keys(this.kanji_db).forEach(
                    (kan) => {
                        if (!(kan in kanjiIndex)) return;
                        kanjiIndex[kan].data.component_subject_ids.forEach((id) => {
                                                                  let rad = subjectIndexRad[id].data.characters;
                                                                  if (rad === null) return;
                                                                  if (this.kanji_db[kan].phonetic.indexOf(rad) === -1) this.kanji_db[kan].semantic = rad;
                                                                  return})
                    })*/
                Object.keys(this.phonetic_db).forEach(
                    (phon) => {
                        Object.keys(this.getPNonCompounds(phon)).forEach(
                            (kan) => {
                                if (this.kanji_db[kan] && this.kanji_db[kan].phonetic && this.kanji_db[kan].phonetic.indexOf(phon) === -1) this.kanji_db[kan].semantic = phon;
                            })
                    })
            },
            // #####################################################################
            // #####################################################################
            getPReadings: function(phon)
            {
                if (phon in this.phonetic_db)
                    return this.phonetic_db[phon].readings;
                else
                    return [];
            },
            // #####################################################################

            // Sort the readings by their importance (main readings first, then the
            // readings that are at least possible, and finally readings unused in
            // jouyou. Also add styles that reflect their importance.
            // #####################################################################
            getPReadings_style: function(phon)
            {
                let result = [];
                const rnd_length = this.phonetic_db[phon].readings.length;

                if (phon in this.phonetic_db)
                {
                    this.phonetic_db[phon].readings.forEach(
                        function(reading, i)
                        {
                            let rnd_count = 0;

                            [phon, ...this.phonetic_db[phon].compounds].forEach(
                                function(compound, j)
                                {
                                    if (!(compound in this.kanji_db))
                                        return;

                                    // Give a bonus to earlier readings in the phonetic's list
                                    if (this.kanji_db[compound].readings[0] === reading)
                                        rnd_count += 10 + (rnd_length-i);
                                    else if (this.kanji_db[compound].readings.includes(reading))
                                        rnd_count += 2 + (rnd_length-i);
                                },
                                this
                            );

                            if (!rnd_count)
                                result.push([rnd_count, `<span class="keisei_obscure_reading">${reading}</span>`]);
                            else if (rnd_count < 10)
                                result.push([rnd_count, `<span class="keisei_alternative_reading">${reading}</span>`]);
                            else
                                result.push([rnd_count, `<span class="keisei_main_reading">${reading}</span>`]);
                        },
                        this
                    );
                }

                return result.sort((a,b)=>b[0]-a[0]).map((d)=>d[1]);
            },
            // #####################################################################
            // #####################################################################
            getPXRefs: function(phon)
            {
                if (phon in this.phonetic_db)
                    return this.phonetic_db[phon].xrefs;
                else
                    return [];
            },
            // #####################################################################
            // #####################################################################
            getPNonCompounds: function(phon)
            {
                if (phon in this.phonetic_db)
                    return this.phonetic_db[phon].non_compounds;
                else
                    return [];
            },
            // #####################################################################

            genWKRadicalToPhon: function()
            {
                Object.keys(this.phonetic_db).forEach(
                    function(phon) {
                        const data = this.phonetic_db[phon];

                        this.wkradical_to_phon[data[`wk-radical`]] = phon;
                        this.wkradical_to_phon[phon] = phon;
                    },
                    this
                );
            },
            // #####################################################################
            mapWKRadicalToPhon: function(radical)
            {
                if (this.wkradical_to_phon && radical in this.wkradical_to_phon)
                    return this.wkradical_to_phon[radical];
                else
                    return null;
            },
            // #####################################################################
            getWKRadical: function(phon)
            {
                return this.phonetic_db[phon][`wk-radical`];
            },
            // #####################################################################
            getWKRadicalPP: function(phon)
            {
                if (this.phonetic_db && this.phonetic_db[phon][`wk-radical`])
                    return _.startCase(this.phonetic_db[phon][`wk-radical`]);
                else
                    return `Not in WK`;
            },
            // #####################################################################

            // #####################################################################
            isKanjiInWK: function(kanji)
            {
                return Boolean(this.wk_kanji_db && (kanji in this.wk_kanji_db));
            },
            // #####################################################################

            // #####################################################################
            isFirstReadingInWK: function(kanji)
            {
                if (this.isKanjiInWK(kanji))
                {
                    const onyomi = this.getWKOnyomi(kanji);

                    return (onyomi.includes(this.getKReadings(kanji)[0]));
                }
                else
                    return true;
            },
            // #####################################################################

            // Check if a kanji is considered obscure in regard to its phonetic,
            // ie, the kanji doesn't look like it has the phonetic at all.
            // #####################################################################
            isPObscure: function(kanji)
            {
                return Boolean(this.kanji_db && (`obscure` in this.kanji_db[kanji]));
            },
            // #####################################################################

            getWKOnyomi: function(kanji)
            {
                if (this.isKanjiInWK(kanji))
                {
                    if (this.wk_kanji_db[kanji].onyomi !== null)
                        return _.map(this.wk_kanji_db[kanji].onyomi.split(`,`), _.trim);
                    else
                        return [];
                }
                else
                    return [`*DB Error*`];
            },
            // #####################################################################
            getWKKMeanings: function(kanji)
            {
                let result = [];

                if (this.isKanjiInWK(kanji))
                    result = _.map(this.wk_kanji_db[kanji].meaning.split(`,`), _.trim);
                else
                {
                    if (this.kanji_db[kanji] === undefined)
                    {
                        console.log(`The kanji ${kanji} is not found in the DB!`);
                        result = [];
                    }
                    else
                        result = this.kanji_db[kanji].meanings;
                }

                return result.map(_.startCase);
            },
            // #####################################################################

            // #####################################################################
            deRendaku: function(reading)
            {
                let result = reading;

                const rendaku = `がぎぐげござじずぜぞだぢちづでどばびぶべぼぱぴぷぺぽ`;
                const vanilla = `かきくけこさしすせそたちしつてとはひふへほはひふへほ`;

                const pattern = _.zip(_.split(rendaku, ``), _.split(vanilla, ``));
                pattern.push([`じょ`, `よ`]);
                pattern.push([`じゃ`, `や`]);
                pattern.push([`じゅ`, `ゆ`]);

                _.forEach(pattern,
                          ([from_kana, to_kana]) =>
                          result = result.replace(from_kana, to_kana)
                         );

                return result;
            },
            // #####################################################################

            // #####################################################################
            // Support for additional pages listing all tone mark
            // #####################################################################

            // #####################################################################
            getPhoneticsByCompCount: function()
            {
                let result = new Map();

                _.forEach(this.phonetic_db,
                          (p_item, phon) => {
                    const comp_len = p_item.compounds.length;

                    if (result.has(comp_len))
                        result.get(comp_len).push(phon);
                    else
                        result.set(comp_len, [phon]);
                }
                         );

                result.forEach(
                    (phons, count) => {
                        result.set(count, phons.sort(
                            (a,b)=>a.localeCompare(b, "ja-u-co-unihan")));
                    }
                );

                return new Map([...result.entries()].sort((a,b)=>b[0]-a[0]));
            },
            // #####################################################################

            // #####################################################################
            getPhoneticsByHeader: function()
            {
                const initials_d = new Map([
                    [`あ`, `あいうえお`],
                    [`か`, `かきくけこがぎぐげご`],
                    [`さ`, `さしすせそざじずぜぞ`],
                    [`た`, `たちつてとだぢづでど`],
                    [`な`, `なにぬねの`],
                    [`は`, `はひふへほばびぶべぼ`],
                    [`ま`, `まみむめも`],
                    [`ら`, `らりるれろ`],
                    [`や`, `やゆよ`],
                    [`わ`, `わを`]
                ]);

                let result = new Map();

                initials_d.forEach(
                    (subheaders, header) => {
                        result.set(header, new Map());

                        _.forEach(subheaders,
                                  (subheader) => {
                            const sub_result = [];

                            _.forEach(this.phonetic_db,
                                      (p_item, phon) => {
                                if (!p_item.readings.length)
                                    return;

                                if (subheader.match(p_item.readings[0][0]))
                                    sub_result.push([2*p_item.compounds.length +
                                                     (p_item["wk-radical"]?1:0),
                                                     phon]);
                            }
                                     );

                            result.get(header).set(subheader,
                                                   sub_result
                                                   .sort((a,b)=>b[0]-a[0])
                                                   .map((d)=>d[1]));
                        }
                                 );
                    },
                    this
                );

                return result;
            }
        };
        // #########################################################################
    }
     ());
    // #############################################################################

    //================================================
    // END code by @acm2010 released under GPL V3 or later license
    //================================================
    let nonPhoneticTypes = {};
    function initNonPhoneticTypes() {
        nonPhoneticTypes[keiseiDB.KTypeEnum.hieroglyph] = true;
        nonPhoneticTypes[keiseiDB.KTypeEnum.indicative] = true;
        nonPhoneticTypes[keiseiDB.KTypeEnum.comp_indicative] = true;
        nonPhoneticTypes[keiseiDB.KTypeEnum.rebus] = true;
        nonPhoneticTypes[keiseiDB.KTypeEnum.kokuji] = true;
        nonPhoneticTypes[keiseiDB.KTypeEnum.shinjitai] = true;
    };

    let keiseiDB;
    // function to be invoked in the startup sequence
    function loadKeiseiDatabase(){
        if (typeof advSearchFilters.kanji_db === 'object'){
            KeiseiDB.prototype.kanji_db = advSearchFilters.kanji_db;
            KeiseiDB.prototype.phonetic_db = advSearchFilters.phonetic_db;
            KeiseiDB.prototype.wk_kanji_db = advSearchFilters.wk_kanji_db;
            keiseiDB = new KeiseiDB();
            keiseiDB.init();
            prepareKeiseiIndexes();
            return Promise.resolve();
        } else {
        let promiseList = [];
            promiseList.push(load_file(kanji_db, true, {responseType: "arraybuffer"}).then(data => {lzmaDecompressAndProcessKanjiDB(data);}));
            promiseList.push(load_file(phonetic_db, true, {responseType: "arraybuffer"}).then(data => {lzmaDecompressAndProcessPhoneticDB(data);}));
            promiseList.push(load_file(wk_kanji_db, true, {responseType: "arraybuffer"}).then(data => {lzmaDecompressAndProcessWkKanjiDB(data);}));
            return Promise.all(promiseList).then(x => {keiseiDB = new KeiseiDB(); keiseiDB.init(); prepareKeiseiIndexes()});
        };
    };

    function lzmaDecompressAndProcessKanjiDB(data){
        let inStream = new LZMA.iStream(data);
        let outStream = LZMA.decompressFile(inStream);
        let string = streamToString(outStream);
        KeiseiDB.prototype.kanji_db = JSON.parse(string);
        advSearchFilters.kanji_db = KeiseiDB.prototype.kanji_db;
    };

    function lzmaDecompressAndProcessPhoneticDB(data){
        let inStream = new LZMA.iStream(data);
        let outStream = LZMA.decompressFile(inStream);
        let string = streamToString(outStream);
        KeiseiDB.prototype.phonetic_db = JSON.parse(string);
        advSearchFilters.phonetic_db = KeiseiDB.prototype.phonetic_db;
    };

    function lzmaDecompressAndProcessWkKanjiDB(data){
        let inStream = new LZMA.iStream(data);
        let outStream = LZMA.decompressFile(inStream);
        let string = streamToString(outStream);
        KeiseiDB.prototype.wk_kanji_db = JSON.parse(string);
        advSearchFilters.wk_kanji_db = KeiseiDB.prototype.wk_kanji_db;
    };

    function deleteKeiseiCache(){
        wkof.file_cache.delete(kanji_db);
        wkof.file_cache.delete(phonetic_db);
        wkof.file_cache.delete(wk_kanji_db);
    };

    var keiseiCompoundIndex = {}
    var keiseiNonCompoundIndex = {}
    var keiseiRelatedMarksIndex = {}

    function prepareKeiseiIndexes(){
        let phonetic = KeiseiDB.prototype.phonetic_db;
        for (let kanji in componentIndexKan){
            if ((typeof KeiseiDB.prototype.phonetic_db[kanji]) === 'object'){
                keiseiCompoundIndex[kanji] = phonetic[kanji].compounds;
                keiseiNonCompoundIndex[kanji] = phonetic[kanji].non_compounds;
                keiseiRelatedMarksIndex[kanji] = phonetic[kanji].xrefs;
            };
        };
        for (let radical in usedInIndexRad){
            if ((typeof KeiseiDB.prototype.phonetic_db[radical]) === 'object'){
                keiseiCompoundIndex[radical] = phonetic[radical].compounds;
                keiseiNonCompoundIndex[radical] = phonetic[radical].non_compounds;
                keiseiRelatedMarksIndex[radical] = phonetic[radical].xrefs;
            };
        };
    };

    // END Keisei Semantic-Phonetic Composition

    // BEGIN Lars Yencken Visually Similar Kanji

    var visuallySimilarData;
    function get_visually_similar_data(){
        if (typeof advSearchFilters.visuallySimilarData === 'object'){
            visuallySimilarData = advSearchFilters.visuallySimilarData;
            return Promise.resolve();
        } else {
            return load_file(visuallySimilarFilename, true, {responseType: "arraybuffer"})
                .then(function(data){lzmaDecompressAndProcessVisuallySimilar(data)});
        };
    };

    function lzmaDecompressAndProcessVisuallySimilar(data){
        let inStream = new LZMA.iStream(data);
        let outStream = LZMA.decompressFile(inStream);
        let string = streamToString(outStream);
        visuallySimilarData = JSON.parse(string);
        advSearchFilters.visuallySimilarData = visuallySimilarData;
    };


    function deleteVisuallySimilarCache(){
        wkof.file_cache.delete(visuallySimilarFilename);
    };

    // END Lars Yencken Visually Similar Kanji

    // BEGIN Niai Visually Similar Kanji
    // Source @acm2010 at the Niai Visually Similar script
    // https://community.wanikani.com/t/userscript-niai-%E4%BC%BC%E5%90%88%E3%81%84-visually-similar-kanji/23325
    // https://github.com/mwil/wanikani-userscripts/tree/master/wanikani-similar-kanji

    // Niai also uses Lars Yencken visually similar data

    var from_keisei_db;
    var old_script_db;
    var wk_niai_noto_db;
    var yl_radical_db;

    function loadNiaiDatabase(){
        if (typeof advSearchFilters.from_keisei_db === 'object'){
            from_keisei_db = advSearchFilters.from_keisei_db;
            old_script_db = advSearchFilters.old_script_db;
            wk_niai_noto_db = advSearchFilters.wk_niai_noto_db;
            yl_radical_db = advSearchFilters.yl_radical_db;
            return Promise.resolve();
        } else {
            let promiseList = [];
            promiseList.push(load_file(from_keisei_db_filename, true, {responseType: "arraybuffer"}).then(data => {lzmaDecompressAndProcessNiai_from_keisei_db(data);}));
            promiseList.push(load_file(old_script_db_filename, true, {responseType: "arraybuffer"}).then(data => {lzmaDecompressAndProcessNiai_old_script_db(data);}));
            promiseList.push(load_file(wk_niai_noto_db_filename, true, {responseType: "arraybuffer"}).then(data => {lzmaDecompressAndProcessNiai_wk_niai_noto_db(data);}));
            promiseList.push(load_file(yl_radical_db_filename, true, {responseType: "arraybuffer"}).then(data => {lzmaDecompressAndProcessNiai_yl_radical_db(data);}));
            return Promise.all(promiseList);
        };
    };

    function lzmaDecompressAndProcessNiai_from_keisei_db(data){
        let inStream = new LZMA.iStream(data);
        let outStream = LZMA.decompressFile(inStream);
        let string = streamToString(outStream);
        from_keisei_db = JSON.parse(string);
        advSearchFilters.from_keisei_db = from_keisei_db;
    };

    function lzmaDecompressAndProcessNiai_old_script_db(data){
        let inStream = new LZMA.iStream(data);
        let outStream = LZMA.decompressFile(inStream);
        let string = streamToString(outStream);
        old_script_db = JSON.parse(string);
        advSearchFilters.old_script_db = old_script_db;
    };

    function lzmaDecompressAndProcessNiai_wk_niai_noto_db(data){
        let inStream = new LZMA.iStream(data);
        let outStream = LZMA.decompressFile(inStream);
        let string = streamToString(outStream);
        wk_niai_noto_db = JSON.parse(string);
        advSearchFilters.wk_niai_noto_db = wk_niai_noto_db;
    };

    function lzmaDecompressAndProcessNiai_yl_radical_db(data){
        let inStream = new LZMA.iStream(data);
        let outStream = LZMA.decompressFile(inStream);
        let string = streamToString(outStream);
        yl_radical_db = JSON.parse(string);
        advSearchFilters.yl_radical_db = yl_radical_db;
    };

    function deleteNiaiCache(){
        wkof.file_cache.delete(from_keisei_db_filename);
        wkof.file_cache.delete(old_script_db_filename);
        wkof.file_cache.delete(wk_niai_noto_db_filename);
        wkof.file_cache.delete(yl_radical_db_filename);
    };

    // END Niai Visually Similar Kanji


    //=============================================================================
    // Begin code lifted from wkof core module and adapted to transfer binary data
    // Must be licensed under MIT license
    //=============================================================================

    //------------------------------
    // Load a file asynchronously, and pass the file as resolved Promise data.
    //------------------------------
    function promise(){var a,b,c=new Promise(function(d,e){a=d;b=e;});c.resolve=a;c.reject=b;return c;};

    function load_file(url, use_cache, options) {
        var fetch_promise = promise();
        var no_cache = split_list(localStorage.getItem('wkof.load_file.nocache') || '');
        if (no_cache.indexOf(url) >= 0 || no_cache.indexOf('*') >= 0) use_cache = false;
        if (use_cache === true) {
            return wkof.file_cache.load(url, use_cache).catch(fetch_url);
        } else {
            return fetch_url();
        };

        // Retrieve file from server
        function fetch_url(){
            var request = new XMLHttpRequest();
            request.onreadystatechange = process_result;
            request.open('GET', url, true);
            if (options.responseType) request.responseType = options.responseType;
            request.send();
            return fetch_promise;
        };

        function process_result(event){
            if (event.target.readyState !== 4) return;
            if (event.target.status >= 400 || event.target.status === 0) return fetch_promise.reject(event.target.status);
            if (use_cache) {
                wkof.file_cache.save(url, event.target.response)
                    .then(fetch_promise.resolve.bind(null,event.target.response));
            } else {
                fetch_promise.resolve(event.target.response);
            };
        };
    };

    //===========================================================================
    // End code lifted from wkof core module and adapted to transfer binary data
    //===========================================================================

    // ----------------------------------------------------------------------
    // Kanjidic2 data
    // ----------------------------------------------------------------------

    var kanjidic2Data;
    function loadKanjidic2(){
        if (typeof advSearchFilters.kanjidic2Data === 'object'){
            kanjidic2Data = advSearchFilters.kanjidic2Data;
            return Promise.resolve();
        } else {
            return load_file(kanjidic2File, true, {responseType: "arraybuffer"})
                .then(function(data){lzmaDecompressAndProcessKanjidic2(data)})
        }
    };

    function lzmaDecompressAndProcessKanjidic2(data){
        let inStream = new LZMA.iStream(data);
        let outStream = LZMA.decompressFile(inStream);
        let string = streamToString(outStream);
        kanjidic2Data = JSON.parse(string);
        advSearchFilters.kanjidic2Data = kanjidic2Data;
    };

    function kanjidic2_cacheDelete(){
        return wkof.file_cache.delete(kanjidic2File);
    };

    // converts the stream to a javascript string
    // the native stream.toString() methof of the LZMA
    // script is abysmally slow and a major source of latency
    function streamToString(outStream){
        var buffers = outStream.buffers, charList = [];
        if (window.TextDecoder){
            // TextDecoder supported - do it the fast way
            var decoder = new TextDecoder();
            for (let n = 0, nL = buffers.length; n < nL; n++) {
                charList.push(decoder.decode(buffers[n]));
            }
        } else {
            // TextDecoder unsupported - do it but a bit slower
            var conv = String.fromCharCode;
            for (let n = 0, nL = buffers.length; n < nL; n++) {
                var buf = buffers[n];
                for (var i = 0, iL = buf.length; i < iL; i++) {
                    charList.push(conv(buf[i]));
                };
            };
        };
        return charList.join('');
    };

    // ----------------------------------------------------------------------
    // Stroke Count for Wanikani radicals
    // ----------------------------------------------------------------------

    var WkStrokeCountData;
    function loadStrokeCount(){
        // check if Item Inspector has already loaded the data
        if (typeof (advSearchFilters) === 'object' && (WkStrokeCountData in advSearchFilters)){
            WkStrokeCountData = advSearchFilters.WkStrokeCountData;
            return Promise.resolve();
        };
        return load_file(Wkit_StrokeCountforRadicals, true, {responseType: "arraybuffer"})
             .then(function(data){lzmaDecompressAndProcessWkStrokeCount(data)})
    };

    function lzmaDecompressAndProcessWkStrokeCount(data){
        let inStream = new LZMA.iStream(data);
        let outStream = LZMA.decompressFile(inStream);
        let string = streamToString(outStream);
        WkStrokeCountData = JSON.parse(string);
        // publish data to Item Inspectort
        if (typeof advSearchFilters === 'object'){
            advSearchFilters.WkStrokeCountData =  WkStrokeCountData;
        };
    };

    function WkStrokeCount_cacheDelete(){
        return wkof.file_cache.delete(Wkit_StrokeCountforRadicals);
    };

    // ----------------------------------------------------------------------
    // A series of functions to add traditional radicals as a source of items
    // ----------------------------------------------------------------------

    var traditionalRadicals;
    var traditionalRadicalsItems;

    function loadTraditionalRadicals(){
        if (typeof advSearchFilters.traditionalRadicalsItems === 'object'){
            traditionalRadicals = advSearchFilters.traditionalRadicals;
            trad_rad_postLoadProcess();
            return Promise.resolve();
        } else {
            return load_file(traditionaRadicalsFile, true, {responseType: "arraybuffer"})
                .then(function(data){lzmaDecompressAndProcessTradRad(data);
                                     advSearchFilters.traditionalRadicals = traditionalRadicals;
                                     trad_rad_postLoadProcess();})
        };
    };

    function lzmaDecompressAndProcessTradRad(data){
        let inStream = new LZMA.iStream(data);
        let outStream = LZMA.decompressFile(inStream);
        let string = streamToString(outStream);
        traditionalRadicals = JSON.parse(string);
    };

    function trad_rad_postLoadProcess(){
        traditionalRadicalsItems = Object.values(traditionalRadicals);
        prepareTradRadIndex();
    };

    function trad_rad_cacheDelete(){
        return wkof.file_cache.delete(traditionaRadicalsFile);
    };

    function tradRadFetcher(){
        return Promise.resolve(traditionalRadicalsItems);
    };

    function registerTraditionalRadicals(){
        if (typeof wkof.ItemData.registry.sources.trad_rad === 'undefined'){
            wkof.ItemData.registry.sources.trad_rad = {
                description: 'Traditional Radicals',
                fetcher: tradRadFetcher,
                options: {},
                filters: {},
            };
        };
        return Promise.resolve();
    };

    var kanjiwithTradRadbyName = {};
    var tradRadInKanji = {};
    function prepareTradRadIndex(){
        for (let item of traditionalRadicalsItems){
            for (let mm of item.data.meanings){
                let meaning = mm.meaning;
                if (typeof kanjiwithTradRadbyName[meaning] !== 'object') kanjiwithTradRadbyName[meaning] = [];
                kanjiwithTradRadbyName[meaning] = kanjiwithTradRadbyName[meaning].concat(item.data.kanji);
            };
            for (let kanji of item.data.kanji){
                if (typeof tradRadInKanji[kanji] !== 'object') tradRadInKanji[kanji] = [];
                tradRadInKanji[kanji].push(item.data.characters);
            };
        };
    };
    //-------------------------------------------
    // Load prerequisite scripts

    function loadPrerequisiteScripts(){
        let promiseList = [];
        promiseList.push(wkof.load_script(lodash_file, true).then(function(){return wkof.wait_state('Wkit_lodash','Ready')}));
        promiseList.push(wkof.load_script(lzma_file, true).then(function(){return wkof.wait_state('Wkit_lzma','Ready')}));
        promiseList.push(wkof.load_script(lzma_shim_file, true).then(function(){return wkof.wait_state('Wkit_lzma_shim','Ready')}));
        return Promise.all(promiseList);
    };

    //------------------------------------------------------------------------------
    // To reduce latency non Wanikani data is loaded on demand.
    // Variables dataRequired and dataLoaded track which data is required and loaded

    var dataRequired = {larsYencken: false, niai: false, keisei: false, kanjidic2: false, items: false, WkStrokeCountData: false,};
    var dataLoaded = {larsYencken: false, niai: false, keisei: false, kanjidic2: false, items: false, WkStrokeCountData: false,};

    function loadMissingData(){
        let promiseList = [];

        if (dataRequired.larsYencken && !dataLoaded.larsYencken) {
            promiseList.push(get_visually_similar_data().then(function(){dataLoaded.larsYencken = true;}));
        };
        if (dataRequired.niai && !dataLoaded.niai) {
            promiseList.push(loadNiaiDatabase().then(function(){dataLoaded.niai = true;}));
        };
        if (dataRequired.keisei && !dataLoaded.keisei) {
            promiseList.push(loadKeiseiDatabase().then(function(){prepareKeiseiIndexes(); dataLoaded.keisei = true;}));
        };
        if (dataRequired.kanjidic2 && !dataLoaded.kanjidic2) {
            promiseList.push(loadKanjidic2().then(function(){dataLoaded.kanjidic2 = true;}));
        };
        if (dataRequired.WkStrokeCountData && !dataLoaded.WkStrokeCountData) {
            promiseList.push(loadStrokeCount().then(function(){dataLoaded.WkStrokeCountData = true;}));
        };
        // Items must always be loaded first because they are prerequisites to indexes
        if (!dataLoaded.items) {
            dataLoaded.items = true; // must be called first, otherwise the data will be loaded twice - once for each data source
            let config = {wk_items: {filters:{}, options:{'subjects': true,}}};
            return wkof.ItemData.get_items(config)
                    .then(prepareIndex)
                    .then(loadTraditionalRadicals)
                    .then(function(){return Promise.all(promiseList)})
                    .then(function(){wkof.set_state('advSearchFilters_items', 'ready')})
        } else {
            return wkof.wait_state('advSearchFilters_items', 'ready')
                    .then(function(){return Promise.all(promiseList)})
        };
    };

    startupSequence();
})(window.wkof);