diff --git a/pd/nw/dialog_prefs.html b/pd/nw/dialog_prefs.html
index e5b7d77533c670e2ae667a99b1aa02a73a7e29ad..8148d0d5c749a533cc7d968e4e14f0e75c7ff62b 100644
--- a/pd/nw/dialog_prefs.html
+++ b/pd/nw/dialog_prefs.html
@@ -302,6 +302,23 @@
               <input type="checkbox" id="save_zoom" name="save_zoom">
               <span data-i18n="prefs.gui.zoom.save_zoom"></span>
             </label>
+            <br/><br/>
+            <span data-i18n="prefs.gui.browser.browser_title"></span>
+            <br/>
+            <label data-i18n="[title]prefs.gui.browser.browser_doc_tt">
+              <input type="checkbox" id="browser_doc" name="browser_doc">
+              <span data-i18n="prefs.gui.browser.browser_doc"></span>
+            </label>
+            <br/>
+            <label data-i18n="[title]prefs.gui.browser.browser_path_tt">
+              <input type="checkbox" id="browser_path" name="browser_path">
+              <span data-i18n="prefs.gui.browser.browser_path"></span>
+            </label>
+            <br/>
+            <label data-i18n="[title]prefs.gui.browser.browser_init_tt">
+              <input type="checkbox" id="browser_init" name="browser_init">
+              <span data-i18n="prefs.gui.browser.browser_init"></span>
+            </label>
           </div>
         </div>
 
@@ -396,8 +413,8 @@ function get_gui_preset() {
     return document.getElementById("gui_preset").selectedOptions[0].value;
 }
 
-function get_save_zoom() {
-    return +document.getElementById("save_zoom").checked;
+function get_bool_elem(elem) {
+    return +document.getElementById(elem).checked;
 }
 
 // startup config data
@@ -606,9 +623,10 @@ function apply(save_prefs) {
         midi_use_alsa ? get_attr("midi-outdev-names", attrs).length : 0
     );
 
-    // Send the gui prefs (currently just the name of the gui preset and the
-    // status of the save-zoom toggle) to Pd
-    pdgui.pdsend("pd gui-prefs", get_gui_preset(), get_save_zoom());
+    // Send the gui prefs (currently just the name of the gui preset, the
+    // status of the save-zoom toggle and various options related to the help
+    // browser) to Pd
+    pdgui.pdsend("pd gui-prefs", get_gui_preset(), get_bool_elem("save_zoom"), get_bool_elem("browser_doc"), get_bool_elem("browser_path"), get_bool_elem("browser_init"));
 
     // Send the startup config data to Pd
     pdgui.pdsend.apply(null, ["pd path-dialog", startup_use_stdpath, startup_verbose].concat(get_path_array()));
@@ -847,7 +865,7 @@ function midi_prefs_callback(attrs) {
     pdgui.resize_window(pd_object_callback);
 }
 
-function gui_prefs_callback(name, save_zoom) {
+function gui_prefs_callback(name, save_zoom, browser_doc, browser_path, browser_init) {
     var s = document.getElementById("gui_preset"),
         i;
     for (i = 0; i < s.options.length; i++) {
@@ -857,6 +875,9 @@ function gui_prefs_callback(name, save_zoom) {
         }
     }
     document.getElementById("save_zoom").checked = !!save_zoom;
+    document.getElementById("browser_doc").checked = !!browser_doc;
+    document.getElementById("browser_path").checked = !!browser_path;
+    document.getElementById("browser_init").checked = !!browser_init;
 }
 
 // startup settings
diff --git a/pd/nw/dialog_search.html b/pd/nw/dialog_search.html
index 53d8e74abcf449940fe6dd2ab02914886d2a8771..19343356d190fb2ea7a896382bbbd5a90b5177be 100644
--- a/pd/nw/dialog_search.html
+++ b/pd/nw/dialog_search.html
@@ -4,27 +4,18 @@
     <link id="page_style" rel="stylesheet"
           type="text/css" href="css/default.css">
     <title>Pd Search Engine</title>
-    <script type="text/javascript" src="./elasticlunr.js"></script>
     <script type="text/javascript" src="./console_search.js"></script>
     <script>
 "use strict";
 var pdgui = require("./pdgui.js");
 var fs = require("fs");
 var path = require("path");
-var dive = require("./dive.js"); // small module to recursively search dirs
 var l = pdgui.get_local_string;
 
-var index = elasticlunr();
+var index;
 
 pdgui.skin.apply(window);
 
-index.addField("title");
-index.addField("keywords");
-index.addField("description");
-//index.addField("body");
-index.addField("path");
-index.setRef("id");
-
 var filetypes = [".pd", ".txt", ".htm", ".html", ".pdf"];
 
 // Table of Contents to start with
@@ -159,54 +150,6 @@ function translate_form() {
     }
 }
 
-function add_doc_to_index(filename, data) {
-    var title = path.basename(filename, ".pd"),
-        big_line = data.replace("\n", " "),
-        keywords,
-        desc;
-        // We use [\s\S] to match across multiple lines...
-        keywords = big_line
-            .match(/#X text \-?[0-9]+ \-?[0-9]+ KEYWORDS ([\s\S]*?);/i),
-        desc = big_line
-            .match(/#X text \-?[0-9]+ \-?[0-9]+ DESCRIPTION ([\s\S]*?);/i);
-        keywords = keywords && keywords.length > 1 ? keywords[1].trim() : null;
-        desc = desc && desc.length > 1 ? desc[1].trim() : null;
-        // Remove the Pd escapes for commas
-        desc = desc ? desc.replace(" \\,", ",") : null;
-        if (desc) {
-            // format Pd's "comma atoms" as normal commas
-            desc = desc.replace(" \\,", ",");
-        }
-    if (title.slice(-5) === "-help") {
-        title = title.slice(0, -5);
-    }
-    index.addDoc({
-        "id": filename,
-        "title": title,
-        "keywords": keywords,
-        "description": desc
-        //"body": big_line,
-    });
-}
-
-function read_file(err, filename, stat) {
-    if (!err) {
-        if (filename.slice(-3) === ".pd") {
-            fs.readFile(filename, { encoding: "utf8", flag: "r" },
-                function(read_err, data) {
-                    if (!read_err) {
-                        add_doc_to_index(filename, data);
-                    } else {
-                        pdgui.post("err: " + read_err);
-                    }
-                }
-            );
-        }
-    } else {
-        pdgui.post("err: " + err);
-    }
-}
-
 function click_toc(dir) {
     document.getElementById("search_text").value = dir;
     doc_search();
@@ -251,18 +194,13 @@ function display_toc() {
     });
 }
 
-function finish_build() {
+function finish_build(idx) {
+    index = idx;
     document.getElementById("search_text").disabled = false;
     clear_results();
     display_toc();
 }
 
-function build_index() {
-    var doc_path = path.join(pdgui.get_lib_dir(), "doc");
-    pdgui.post("doc path is " + doc_path);
-    dive(doc_path, read_file, finish_build);
-}
-
 function clear_results() {
     document.getElementById("results").innerHTML = "";
 }
@@ -528,7 +466,7 @@ function register_window_id(id, attrs) {
     document.getElementById("search_text").disabled = true;
     document.getElementById("file_browser").setAttribute("nwworkingdir",
         doc_path); // Probably need a doc getter in pdgui
-    build_index();
+    pdgui.build_index(finish_build);
 }
 
 function display_no_results() {
diff --git a/pd/nw/locales/de/translation.json b/pd/nw/locales/de/translation.json
index 6b0ea4e01febdba7a7e0ea4addc83c8668359a81..f382fa17c9889c466db4412024d44700bf72664e 100644
--- a/pd/nw/locales/de/translation.json
+++ b/pd/nw/locales/de/translation.json
@@ -407,6 +407,15 @@
       "zoom": {
         "save_zoom": "Speichern/Laden der Vergrößerung im Patch",
         "save_zoom_tt": "Speichere die aktuelle Vergrößerung mit dem Patch und stelle diese beim Laden des Patches wieder her"
+      },
+      "browser": {
+        "browser_title": "Hilfe-Browser-Einstellungen (WARNUNG: Änderungen können Startup-Zeiten beeinflussen!)",
+        "browser_doc": "Hilfe-Browser durchsucht nur den doc-Ordner",
+        "browser_doc_tt": "Der Hilfe-Browser durchkämmt nur Hilfe-Patches im doc-Ordner nach Suchbegriffen (schneller)",
+        "browser_path": "Hilfe-Browser durchsucht auch den Hilfe-Pfad",
+        "browser_path_tt": "Der Hilfe-Browser durchkämmt auch Hilfe-Patches im benutzerdefinierten Hilfe-Pfad nach Suchbegriffen (langsamer)",
+        "browser_init": "Erstelle den Hilfe-Index beim Programmstart",
+        "browser_init_tt": "Falls aktiviert, erstelle den Index für den Hilfe-Browser bereits beim Programmstart"
       }
     },
     "audio": {
diff --git a/pd/nw/locales/en/translation.json b/pd/nw/locales/en/translation.json
index 2032010d8526513fd8ca91f3dd4843836ed8a4cd..c779742a70f17f466865b89090416169b593fd64 100644
--- a/pd/nw/locales/en/translation.json
+++ b/pd/nw/locales/en/translation.json
@@ -408,6 +408,15 @@
       "zoom": {
         "save_zoom": "save/load zoom level with patch",
         "save_zoom_tt": "Save the current zoom level with the patch and restore it when reloading the patch"
+      },
+      "browser": {
+        "browser_title": "Help browser settings (WARNING: changing these may affect startup times!)",
+        "browser_doc": "help browser only searches the doc folder",
+        "browser_doc_tt": "Only scan help patches in the doc folder for searchable keywords (faster)",
+        "browser_path": "help browser also searches the help path",
+        "browser_path_tt": "Also scan help patches in the user-defined help path for searchable keywords (slower)",
+        "browser_init": "prepare the help index at application start",
+        "browser_init_tt": "If checked, prepare the index for the help browser already when the application starts"
       }
     },
     "audio": {
diff --git a/pd/nw/pdgui.js b/pd/nw/pdgui.js
index 08a1247482e13402eb9dae9da82be652d3526675..7f3bffa474585e25a182bf0d711651e32d1a29f3 100644
--- a/pd/nw/pdgui.js
+++ b/pd/nw/pdgui.js
@@ -2,6 +2,7 @@
 
 var pwd;
 var lib_dir;
+var help_path, browser_doc, browser_path, browser_init;
 var pd_engine_id;
 
 exports.set_pwd = function(pwd_string) {
@@ -26,6 +27,19 @@ exports.set_pd_engine_id = function (id) {
 
 exports.defunkify_windows_path = defunkify_windows_path;
 
+function gui_set_browser_config(doc_flag, path_flag, init_flag, helppath) {
+    // post("gui_set_browser_config: " + helppath.join(":"));
+    browser_doc = doc_flag;
+    browser_path = path_flag;
+    browser_init = init_flag;
+    help_path = helppath;
+    // AG: Start building the keyword index for dialog_search.html. We do this
+    // here so that we can be sure that lib_dir and help_path are known already.
+    // (This may also be deferred until the browser is launched for the first
+    // time, depending on the value of browser_init.)
+    if (browser_init == 1) make_index();
+}
+
 function gui_set_lib_dir(dir) {
     lib_dir = dir;
 }
@@ -60,10 +74,140 @@ exports.set_focused_patchwin = function(cid) {
     last_focused = cid;
 }
 
+// Keyword index (cf. dialog_search.html)
+
+var fs = require("fs");
+var path = require("path");
+var dive = require("./dive.js"); // small module to recursively search dirs
+var elasticlunr = require("./elasticlunr.js"); // lightweight full-text search engine in JavaScript, cf. https://github.com/weixsong/elasticlunr.js/
+
+var index = elasticlunr();
+
+index.addField("title");
+index.addField("keywords");
+index.addField("description");
+//index.addField("body");
+index.addField("path");
+index.setRef("id");
+
+function add_doc_to_index(filename, data) {
+    var title = path.basename(filename, ".pd"),
+        big_line = data.replace("\n", " "),
+        keywords,
+        desc;
+        // We use [\s\S] to match across multiple lines...
+        keywords = big_line
+            .match(/#X text \-?[0-9]+ \-?[0-9]+ KEYWORDS ([\s\S]*?);/i),
+        desc = big_line
+            .match(/#X text \-?[0-9]+ \-?[0-9]+ DESCRIPTION ([\s\S]*?);/i);
+        keywords = keywords && keywords.length > 1 ? keywords[1].trim() : null;
+        desc = desc && desc.length > 1 ? desc[1].trim() : null;
+        // Remove the Pd escapes for commas
+        desc = desc ? desc.replace(" \\,", ",") : null;
+        if (desc) {
+            // format Pd's "comma atoms" as normal commas
+            desc = desc.replace(" \\,", ",");
+        }
+    if (title.slice(-5) === "-help") {
+        title = title.slice(0, -5);
+    }
+    index.addDoc({
+        "id": filename,
+        "title": title,
+        "keywords": keywords,
+        "description": desc
+        //"body": big_line,
+    });
+}
+
+function read_file(err, filename, stat) {
+    if (!err) {
+        if (filename.slice(-3) === ".pd") {
+            // AG: We MUST read the files synchronously here. This might be a
+            // performance issue on some systems, but if we don't do this then
+            // we may open a huge number of files simultaneously, causing the
+            // process to run out of file handles.
+            try {
+                var data = fs.readFileSync(filename, { encoding: "utf8", flag: "r" });
+                add_doc_to_index(filename, data);
+            } catch (read_err) {
+                post("err: " + read_err);
+            }
+        }
+    } else {
+        // AG: Simply ignore missing/unreadable files and directories.
+        // post("err: " + err);
+    }
+}
+
+var index_done = false;
+var index_started = false;
+
+function finish_index() {
+    index_done = true;
+    post("finished building help index");
+}
+
+// AG: pilfered from https://stackoverflow.com/questions/21077670
+function expand_tilde(filepath) {
+    if (filepath[0] === '~') {
+        return path.join(process.env.HOME, filepath.slice(1));
+    }
+    return filepath;
+}
+
+// AG: This is supposed to be executed only once, after lib_dir has been set.
+// Note that dive() traverses lib_dir asynchronously, so we report back in
+// finish_index() when this is done.
+function make_index() {
+    var doc_path = browser_doc?path.join(lib_dir, "doc"):lib_dir;
+    var i = 0;
+    var l = help_path.length;
+    function make_index_cont() {
+        if (i < l) {
+            var doc_path = help_path[i++];
+            // AG: These paths might not exist, ignore them in this case. Also
+            // note that we need to expand ~ here.
+            var full_path = expand_tilde(doc_path);
+            fs.lstat(full_path, function(err, stat) {
+                if (!err) {
+                    post("building help index in " + doc_path);
+                    dive(full_path, read_file, make_index_cont);
+                } else {
+                    make_index_cont();
+                }
+            });
+        } else {
+            finish_index();
+        }
+    }
+    index_started = true;
+    post("building help index in " + doc_path);
+    dive(doc_path, read_file, browser_path?make_index_cont:finish_index);
+}
+
+// AG: This is called from dialog_search.html with a callback that expects to
+// receive the finished index as its sole argument. We also build the index
+// here if needed, using make_index, then simply wait until make_index
+// finishes and finally invoke the callback on the resulting index.
+function build_index(cb) {
+    function build_index_worker() {
+        if (index_done == true) {
+            cb(index);
+        } else {
+            setTimeout(build_index_worker, 500);
+        }
+    }
+    if (index_started == false) {
+        make_index();
+    }
+    build_index_worker();
+}
+
+exports.build_index = build_index;
+
 // Modules
 
-var fs = require("fs");     // for fs.existsSync
-var path = require("path"); // for path.dirname path.extname path.join
 var cp = require("child_process"); // for starting core Pd from GUI in OSX
 
 var parse_svg_path = require("./parse-svg-path.js");
@@ -4964,9 +5108,9 @@ function gui_midi_properties(gfxstub, sys_indevs, sys_outdevs,
     }
 }
 
-function gui_gui_properties(dummy, name, save_zoom) {
+function gui_gui_properties(dummy, name, save_zoom, browser_doc, browser_path, browser_init) {
     if (dialogwin["prefs"] !== null) {
-        dialogwin["prefs"].window.gui_prefs_callback(name, save_zoom);
+        dialogwin["prefs"].window.gui_prefs_callback(name, save_zoom, browser_doc, browser_path, browser_init);
     }
 }
 
diff --git a/pd/src/m_glob.c b/pd/src/m_glob.c
index 87f00fb38df85531c42ee47c8f0c2a6a76f19738..42e135d0de56f110f0356e33382bfd4b5916d787 100644
--- a/pd/src/m_glob.c
+++ b/pd/src/m_glob.c
@@ -75,18 +75,21 @@ static void glob_perf(t_pd *dummy, float f)
     sys_perf = (f != 0);
 }
 
-extern int sys_zoom;
+extern int sys_zoom, sys_browser_doc, sys_browser_path, sys_browser_init;
 extern t_symbol *sys_gui_preset;
-static void glob_gui_prefs(t_pd *dummy, t_symbol *s, float f)
+static void glob_gui_prefs(t_pd *dummy, t_symbol *s, float f, float f2, float f3, float f4)
 {
     sys_gui_preset = s;
     sys_zoom = !!(int)f;
+    sys_browser_doc = !!(int)f2;
+    sys_browser_path = !!(int)f3;
+    sys_browser_init = !!(int)f4;
 }
 
-/* just the gui-preset and the save-zoom toggle for now */
+/* just the gui-preset, the save-zoom toggle and various help browser options for now */
 static void glob_gui_properties(t_pd *dummy)
 {
-    gui_vmess("gui_gui_properties", "xsi", 0, sys_gui_preset->s_name, sys_zoom);
+    gui_vmess("gui_gui_properties", "xsiiii", 0, sys_gui_preset->s_name, sys_zoom, sys_browser_doc, sys_browser_path, sys_browser_init);
 }
 
 // ths one lives inside g_editor so that it can access the clipboard
@@ -172,7 +175,7 @@ void glob_init(void)
     class_addmethod(glob_pdobject, (t_method)glob_clipboard_text,
         gensym("clipboardtext"), A_FLOAT, 0);
     class_addmethod(glob_pdobject, (t_method)glob_gui_prefs,
-        gensym("gui-prefs"), A_SYMBOL, A_FLOAT, 0);
+        gensym("gui-prefs"), A_SYMBOL, A_FLOAT, A_FLOAT, A_FLOAT, A_FLOAT, 0);
     class_addmethod(glob_pdobject, (t_method)glob_gui_properties,
         gensym("gui-properties"), 0);
     class_addmethod(glob_pdobject, (t_method)glob_recent_files,
diff --git a/pd/src/s_file.c b/pd/src/s_file.c
index 22d660f29af432a69c10196dfd8d1f5da9c95fcc..30a39d61aa69f99c4db46f78e38ed3450107e309 100644
--- a/pd/src/s_file.c
+++ b/pd/src/s_file.c
@@ -40,7 +40,7 @@
 #define snprintf sprintf_s
 #endif
 
-int sys_defeatrt, sys_zoom;
+int sys_defeatrt, sys_zoom, sys_browser_doc = 1, sys_browser_path, sys_browser_init;
 t_symbol *sys_flags = &s_;
 void sys_doflags( void);
 
@@ -669,6 +669,12 @@ void sys_loadpreferences( void)
         sscanf(prefbuf, "%d", &sys_defeatrt);
     if (sys_getpreference("savezoom", prefbuf, MAXPDSTRING))
         sscanf(prefbuf, "%d", &sys_zoom);
+    if (sys_getpreference("browser_doc", prefbuf, MAXPDSTRING))
+        sscanf(prefbuf, "%d", &sys_browser_doc);
+    if (sys_getpreference("browser_path", prefbuf, MAXPDSTRING))
+        sscanf(prefbuf, "%d", &sys_browser_path);
+    if (sys_getpreference("browser_init", prefbuf, MAXPDSTRING))
+        sscanf(prefbuf, "%d", &sys_browser_init);
     if (sys_getpreference("guipreset", prefbuf, MAXPDSTRING))
     {
         char preset_buf[MAXPDSTRING];
@@ -808,6 +814,12 @@ void glob_savepreferences(t_pd *dummy)
     sys_putpreference("defeatrt", buf1);
     sprintf(buf1, "%d", sys_zoom);
     sys_putpreference("savezoom", buf1);
+    sprintf(buf1, "%d", sys_browser_doc);
+    sys_putpreference("browser_doc", buf1);
+    sprintf(buf1, "%d", sys_browser_path);
+    sys_putpreference("browser_path", buf1);
+    sprintf(buf1, "%d", sys_browser_init);
+    sys_putpreference("browser_init", buf1);
     sys_putpreference("guipreset", sys_gui_preset->s_name);
     sys_putpreference("flags", 
         (sys_flags ? sys_flags->s_name : ""));
diff --git a/pd/src/s_main.c b/pd/src/s_main.c
index 2f2f0d821bfdfe0020b5537c4cec0d3d74c1aeb5..42a2e59f12136c820c5b2391aff20594fd7ad59d 100644
--- a/pd/src/s_main.c
+++ b/pd/src/s_main.c
@@ -285,11 +285,13 @@ void glob_forward_files_from_secondary_instance(void)
 }
 
 extern void glob_recent_files(t_pd *dummy);
+extern int sys_browser_doc, sys_browser_path, sys_browser_init;
 
 /* this is called from main() in s_entry.c */
 int sys_main(int argc, char **argv)
 {
     int i, noprefs;
+    t_namelist *nl;
     sys_externalschedlib = 0;
     sys_extraflags = 0;
     sys_gui_preset = gensym("default");
@@ -325,6 +327,17 @@ int sys_main(int argc, char **argv)
     gui_vmess("gui_set_gui_preset", "s", sys_gui_preset->s_name);
         /* send the recent files list */
     glob_recent_files(0);
+        /* AG: send the browser config; this must come *after* gui_set_lib_dir
+           so that the lib_dir is available when help indexing starts */
+    gui_start_vmess("gui_set_browser_config", "iii",
+                    sys_browser_doc, sys_browser_path, sys_browser_init);
+    gui_start_array();
+    for (nl = sys_helppath; nl; nl = nl->nl_next)
+    {
+        gui_s(nl->nl_string);
+    }
+    gui_end_array();
+    gui_end_vmess();
 
     if (sys_externalschedlib)
         return (sys_run_scheduler(sys_externalschedlibname,