Index: lib/matcher.js
===================================================================
--- a/lib/matcher.js
+++ b/lib/matcher.js
@@ -295,36 +295,45 @@
    * Checks whether the entries for a particular keyword match a URL
    * @param {string} keyword
    * @param {string} location
    * @param {number} typeMask
    * @param {string} [docDomain]
    * @param {boolean} [thirdParty]
    * @param {string} [sitekey]
    * @param {boolean} [specificOnly]
+   * @param {?Array.<Filter>} [collection] An optional list of filters to which
+   *   to append any results. If specified, the function adds <em>all</em>
+   *   matching filters to the list; if omitted, the function directly returns
+   *   the first matching filter.
    * @returns {?Filter}
    * @protected
    */
   checkEntryMatch(keyword, location, typeMask, docDomain, thirdParty, sitekey,
-                  specificOnly)
+                  specificOnly, collection)
   {
     // We need to skip the simple (location-only) filters if the type mask does
     // not contain any default content types.
     if ((typeMask & DEFAULT_TYPES) != 0)
     {
       let simpleSet = this._simpleFiltersByKeyword.get(keyword);
       if (simpleSet)
       {
         for (let filter of simpleSet)
         {
           if (specificOnly && !(filter instanceof WhitelistFilter))
             continue;
 
           if (filter.matchesLocation(location))
-            return filter;
+          {
+            if (!collection)
+              return filter;
+
+            collection.push(filter);
+          }
         }
       }
     }
 
     let complexSet = null;
 
     // If the type mask contains a non-default type (first condition) and it is
     // the only type in the mask (second condition), we can use the
@@ -346,17 +355,22 @@
     {
       for (let filter of complexSet)
       {
         if (specificOnly && filter.isGeneric() &&
             !(filter instanceof WhitelistFilter))
           continue;
 
         if (filter.matches(location, typeMask, docDomain, thirdParty, sitekey))
-          return filter;
+        {
+          if (!collection)
+            return filter;
+
+          collection.push(filter);
+        }
       }
     }
 
     return null;
   }
 
   /**
    * Tests whether the URL matches any of the known filters
@@ -525,16 +539,60 @@
                                                        typeMask, docDomain,
                                                        thirdParty, sitekey);
       }
     }
 
     return whitelistHit || blacklistHit;
   }
 
+  _searchInternal(location, typeMask, docDomain, thirdParty, sitekey,
+                  specificOnly, filterType)
+  {
+    let hits = {};
+
+    let searchBlocking = filterType == "blocking" || filterType == "all";
+    let searchWhitelist = filterType == "whitelist" || filterType == "all";
+
+    if (searchBlocking)
+      hits.blocking = [];
+
+    if (searchWhitelist)
+      hits.whitelist = [];
+
+    // If the type mask includes no types other than whitelist-only types, we
+    // can skip the blacklist.
+    if ((typeMask & ~WHITELIST_ONLY_TYPES) == 0)
+      searchBlocking = false;
+
+    let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g);
+    if (candidates === null)
+      candidates = [];
+    candidates.push("");
+
+    for (let i = 0, l = candidates.length; i < l; i++)
+    {
+      if (searchBlocking)
+      {
+        this._blacklist.checkEntryMatch(candidates[i], location, typeMask,
+                                        docDomain, thirdParty, sitekey,
+                                        specificOnly, hits.blocking);
+      }
+
+      if (searchWhitelist)
+      {
+        this._whitelist.checkEntryMatch(candidates[i], location, typeMask,
+                                        docDomain, thirdParty, sitekey,
+                                        false, hits.whitelist);
+      }
+    }
+
+    return hits;
+  }
+
   /**
    * @see Matcher#matchesAny
    * @inheritdoc
    */
   matchesAny(location, typeMask, docDomain, thirdParty, sitekey, specificOnly)
   {
     let key = location + " " + typeMask + " " + docDomain + " " + thirdParty +
       " " + sitekey + " " + specificOnly;
@@ -550,16 +608,62 @@
       this._resultCache.clear();
 
     this._resultCache.set(key, result);
 
     return result;
   }
 
   /**
+   * @typedef {object} MatcherSearchResults
+   * @property {Array.<BlockingFilter>} [blocking] List of blocking filters
+   *   found.
+   * @property {Array.<WhitelistFilter>} [whitelist] List of whitelist filters
+   *   found.
+   */
+
+  /**
+   * Searches all blocking and whitelist filters and returns results matching
+   * the given parameters.
+   *
+   * @param {string} location
+   * @param {number} typeMask
+   * @param {string} [docDomain]
+   * @param {boolean} [thirdParty]
+   * @param {string} [sitekey]
+   * @param {boolean} [specificOnly]
+   * @param {string} [filterType] The types of filters to look for. This can be
+   *   <code>"blocking"</code>, <code>"whitelist"</code>, or
+   *   <code>"all"</code> (default).
+   *
+   * @returns {MatcherSearchResults}
+   */
+  search(location, typeMask, docDomain, thirdParty, sitekey, specificOnly,
+         filterType = "all")
+  {
+    let key = "* " + location + " " + typeMask + " " + docDomain + " " +
+              thirdParty + " " + sitekey + " " + specificOnly + " " +
+              filterType;
+
+    let result = this._resultCache.get(key);
+    if (typeof result != "undefined")
+      return result;
+
+    result = this._searchInternal(location, typeMask, docDomain, thirdParty,
+                                  sitekey, specificOnly, filterType);
+
+    if (this._resultCache.size >= this.maxCacheEntries)
+      this._resultCache.clear();
+
+    this._resultCache.set(key, result);
+
+    return result;
+  }
+
+  /**
    * Tests whether the URL is whitelisted
    * @see Matcher#matchesAny
    * @inheritdoc
    * @returns {boolean}
    */
   isWhitelisted(location, typeMask, docDomain, thirdParty, sitekey,
                 specificOnly)
   {
