[Pkg-javascript-commits] [node-fuzzaldrin-plus] 02/17: New upstream version 0.3.1+git.20161008.da2cb58

Praveen Arimbrathodiyil praveen at moszumanska.debian.org
Sat Nov 19 18:46:38 UTC 2016


This is an automated email from the git hooks/post-receive script.

praveen pushed a commit to branch master
in repository node-fuzzaldrin-plus.

commit 9931127b0fa1802c5c113849943a8e355e651293
Author: Praveen Arimbrathodiyil <praveen at debian.org>
Date:   Sat Nov 19 23:33:48 2016 +0530

    New upstream version 0.3.1+git.20161008.da2cb58
---
 Gruntfile.coffee                    |   31 +-
 LICENSE.md                          |    1 -
 benchmark/benchmark.coffee          |   59 +-
 demo/demo.css                       |  123 +++++
 demo/demo.html                      |   90 ++++
 demo/movies.json                    | 1005 +++++++++++++++++++++++++++++++++++
 dist-browser/fuzzaldrin-plus.js     |  914 +++++++++++++++++++++++++++++++
 dist-browser/fuzzaldrin-plus.min.js |    2 +
 package.json                        |   16 +-
 spec/filter-spec.coffee             |  199 +++++--
 spec/legacy-filter-spec.coffee      |  142 -----
 src/filter.coffee                   |   45 +-
 src/fuzzaldrin.coffee               |   92 ++--
 src/legacy.coffee                   |  118 ----
 src/matcher.coffee                  |  123 ++++-
 src/pathScorer.coffee               |  132 +++++
 src/query.coffee                    |   69 +++
 src/scorer.coffee                   |  231 +++-----
 18 files changed, 2796 insertions(+), 596 deletions(-)

diff --git a/Gruntfile.coffee b/Gruntfile.coffee
index 45c521c..a170e6e 100644
--- a/Gruntfile.coffee
+++ b/Gruntfile.coffee
@@ -21,6 +21,30 @@ module.exports = (grunt) ->
       test: ['spec/*.coffee']
       gruntfile: ['Gruntfile.coffee']
 
+    browserify:
+
+      options:
+        banner: '/* <%= pkg.name %> - v<%= pkg.version %> - @license: <%= pkg.license %>; @author: Jean Christophe Roy; @site: <%= pkg.homepage %> */\n'
+        browserifyOptions:
+          standalone: 'fuzzaldrin'
+
+      dist:
+        src: ['lib/fuzzaldrin.js']
+        dest: 'dist-browser/fuzzaldrin-plus.js'
+
+
+    uglify:
+
+      options:
+        preserveComments: false
+        banner: '/* <%= pkg.name %> - v<%= pkg.version %> - @license: <%= pkg.license %>; @author: Jean Christophe Roy; @site: <%= pkg.homepage %> */\n'
+
+      dist:
+        src: 'dist-browser/fuzzaldrin-plus.js',
+        dest: 'dist-browser/fuzzaldrin-plus.min.js'
+
+
+
     shell:
       test:
         command: 'node node_modules/jasmine-focused/bin/jasmine-focused --coffee --captureExceptions spec'
@@ -32,6 +56,9 @@ module.exports = (grunt) ->
   grunt.loadNpmTasks('grunt-contrib-coffee')
   grunt.loadNpmTasks('grunt-shell')
   grunt.loadNpmTasks('grunt-coffeelint')
+  grunt.loadNpmTasks('grunt-browserify')
+  grunt.loadNpmTasks('grunt-contrib-uglify')
+
 
   grunt.registerTask 'clean', ->
     rm = (pathToDelete) ->
@@ -40,5 +67,7 @@ module.exports = (grunt) ->
 
   grunt.registerTask('lint', ['coffeelint'])
   grunt.registerTask('test', ['default', 'shell:test'])
-  grunt.registerTask('prepublish', ['clean', 'test'])
+  grunt.registerTask('prepublish', ['clean', 'test', 'distribute'])
   grunt.registerTask('default', ['coffee', 'lint'])
+  grunt.registerTask('distribute', ['default', 'browserify', 'uglify'])
+
diff --git a/LICENSE.md b/LICENSE.md
index 81f8b50..552d54c 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,5 +1,4 @@
 Copyright (c) 2015 Jean Christophe Roy
-Copyright (c) 2013 GitHub Inc.
 
 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
diff --git a/benchmark/benchmark.coffee b/benchmark/benchmark.coffee
index f532bd9..cdca07a 100644
--- a/benchmark/benchmark.coffee
+++ b/benchmark/benchmark.coffee
@@ -1,23 +1,21 @@
 fs = require 'fs'
 path = require 'path'
 
-{filter, match, prepQuery} = require '../src/fuzzaldrin'
+fuzzaldrinPlus = require '../src/fuzzaldrin'
+legacy = require 'fuzzaldrin'
 
 lines = fs.readFileSync(path.join(__dirname, 'data.txt'), 'utf8').trim().split('\n')
-
-forceAllMatch = {maxInners:-1}
-legacy = {legacy:true}
-mitigation = {maxInners:Math.floor(0.2*lines.length)}
+forceAllMatch = {maxInners: -1}
+mitigation = {maxInners: Math.floor(0.2 * lines.length)}
 
 #warmup + compile
-filter(lines, 'index', forceAllMatch)
-filter(lines, 'index', legacy)
-
+fuzzaldrinPlus.filter(lines, 'index', forceAllMatch)
+legacy.filter(lines, 'index')
 
 console.log("======")
 
 startTime = Date.now()
-results = filter(lines, 'index')
+results = fuzzaldrinPlus.filter(lines, 'index')
 console.log("Filtering #{lines.length} entries for 'index' took #{Date.now() - startTime}ms for #{results.length} results (~10% of results are positive, mix exact & fuzzy)")
 
 if results.length isnt 6168
@@ -25,75 +23,75 @@ if results.length isnt 6168
   process.exit(1)
 
 startTime = Date.now()
-results = filter(lines, 'index', legacy)
+results = legacy.filter(lines, 'index')
 console.log("Filtering #{lines.length} entries for 'index' took #{Date.now() - startTime}ms for #{results.length} results (~10% of results are positive, Legacy method)")
 
 
 console.log("======")
 
 startTime = Date.now()
-results = filter(lines, 'indx')
+results = fuzzaldrinPlus.filter(lines, 'indx')
 console.log("Filtering #{lines.length} entries for 'indx' took #{Date.now() - startTime}ms for #{results.length} results (~10% of results are positive, Fuzzy match)")
 
 startTime = Date.now()
-results = filter(lines, 'indx', legacy)
+results = legacy.filter(lines, 'indx')
 console.log("Filtering #{lines.length} entries for 'indx' took #{Date.now() - startTime}ms for #{results.length} results (~10% of results are positive, Fuzzy match, Legacy)")
 
 console.log("======")
 
 startTime = Date.now()
-results = filter(lines, 'walkdr')
+results = fuzzaldrinPlus.filter(lines, 'walkdr')
 console.log("Filtering #{lines.length} entries for 'walkdr' took #{Date.now() - startTime}ms for #{results.length} results (~1% of results are positive, fuzzy)")
 
 startTime = Date.now()
-results = filter(lines, 'walkdr', legacy)
+results = legacy.filter(lines, 'walkdr')
 console.log("Filtering #{lines.length} entries for 'walkdr' took #{Date.now() - startTime}ms for #{results.length} results (~1% of results are positive, Legacy method)")
 
 
 console.log("======")
 
 startTime = Date.now()
-results = filter(lines, 'node', forceAllMatch)
+results = fuzzaldrinPlus.filter(lines, 'node', forceAllMatch)
 console.log("Filtering #{lines.length} entries for 'node' took #{Date.now() - startTime}ms for #{results.length} results (~98% of results are positive, mostly Exact match)")
 
 startTime = Date.now()
-results = filter(lines, 'node', legacy)
+results = legacy.filter(lines, 'node')
 console.log("Filtering #{lines.length} entries for 'node' took #{Date.now() - startTime}ms for #{results.length} results (~98% of results are positive, mostly Exact match, Legacy method)")
 
 
 console.log("======")
 
 startTime = Date.now()
-results = filter(lines, 'nm', forceAllMatch)
+results = fuzzaldrinPlus.filter(lines, 'nm', forceAllMatch)
 console.log("Filtering #{lines.length} entries for 'nm' took #{Date.now() - startTime}ms for #{results.length} results (~98% of results are positive, Acronym match)")
 
 startTime = Date.now()
-results = filter(lines, 'nm', forceAllMatch)
+results = legacy.filter(lines, 'nm')
 console.log("Filtering #{lines.length} entries for 'nm' took #{Date.now() - startTime}ms for #{results.length} results (~98% of results are positive, Acronym match, Legacy method)")
 
 
 console.log("======")
 
 startTime = Date.now()
-results = filter(lines, 'nodemodules', forceAllMatch)
+results = fuzzaldrinPlus.filter(lines, 'nodemodules', forceAllMatch)
 console.log("Filtering #{lines.length} entries for 'nodemodules' took #{Date.now() - startTime}ms for #{results.length} results (~98% positive + Fuzzy match, [Worst case scenario])")
 
 startTime = Date.now()
-results = filter(lines, 'nodemodules', mitigation)
+results = fuzzaldrinPlus.filter(lines, 'nodemodules', mitigation)
 console.log("Filtering #{lines.length} entries for 'nodemodules' took #{Date.now() - startTime}ms for #{results.length} results (~98% positive + Fuzzy match, [Mitigation])")
 
 startTime = Date.now()
-results = filter(lines, 'nodemodules', legacy)
+results = legacy.filter(lines, 'nodemodules')
 console.log("Filtering #{lines.length} entries for 'nodemodules' took #{Date.now() - startTime}ms for #{results.length} results (Legacy)")
 
 console.log("======")
 
 startTime = Date.now()
-results = filter(lines, 'ndem', forceAllMatch)
+results = fuzzaldrinPlus.filter(lines, 'ndem', forceAllMatch)
 console.log("Filtering #{lines.length} entries for 'ndem' took #{Date.now() - startTime}ms for #{results.length} results (~98% positive + Fuzzy match, [Worst case but shorter srting])")
 
 startTime = Date.now()
-results = filter(lines, 'ndem', legacy)
+results = legacy.filter(lines, 'ndem')
 console.log("Filtering #{lines.length} entries for 'ndem' took #{Date.now() - startTime}ms for #{results.length} results (Legacy)")
 
 
@@ -101,11 +99,16 @@ console.log("======")
 
 startTime = Date.now()
 query = 'index'
-prepared = prepQuery(query)
-match(line, query, prepared) for line in lines
-console.log("Matching #{results.length} results for 'index' took #{Date.now() - startTime}ms (Prepare in advance)")
+prepared = fuzzaldrinPlus.prepareQuery(query)
+fuzzaldrinPlus.match(line, query, {preparedQuery: prepared}) for line in lines
+console.log("Matching #{lines.length} results for 'index' took #{Date.now() - startTime}ms (Prepare in advance)")
+
+startTime = Date.now()
+fuzzaldrinPlus.match(line, query) for line in lines
+console.log("Matching #{lines.length} results for 'index' took #{Date.now() - startTime}ms (cache)")
+# replace by `prepQuery ?= scorer.prepQuery(query)`to test without cache.
 
 startTime = Date.now()
-match(line, query) for line in lines
-console.log("Matching #{results.length} results for 'index' took #{Date.now() - startTime}ms (cache)")
+legacy.match(line, query) for line in lines
+console.log("Matching #{lines.length} results for 'index' took #{Date.now() - startTime}ms (legacy)")
 # replace by `prepQuery ?= scorer.prepQuery(query)`to test without cache.
diff --git a/demo/demo.css b/demo/demo.css
new file mode 100644
index 0000000..e6c5e65
--- /dev/null
+++ b/demo/demo.css
@@ -0,0 +1,123 @@
+
+* {
+    box-sizing: border-box;
+}
+
+h1, h2, p, form, label {
+    text-align: center;
+}
+
+h1, h2 {
+    margin-top: 1.5em;
+    margin-bottom: 0.5em;
+}
+
+#sourcetxt {
+    display: block;
+    margin-left: auto;
+    margin-right: auto;
+    width: 90%;
+    max-width: 700px;
+    height: 250px;
+}
+
+.center {
+    display: block;
+    margin-left: auto;
+    margin-right: auto;
+    width: 90%;
+    max-width: 700px;
+}
+
+.typeahead {
+    width: 90%;
+    max-width: 700px;
+    margin: auto;
+    display: block;
+    padding-bottom: 30px;
+    position: relative;
+}
+
+.typeahead input {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    max-width: 700px;
+    padding: 10px;
+    opacity: 1;
+    background: none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255);
+}
+
+.twitter-typeahead {
+    width: 100%;
+}
+
+.twitter-typeahead .tt-query,
+.twitter-typeahead .tt-hint {
+    margin-bottom: 0;
+}
+
+.typeahead ul, .typeahead li {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+    border: 0;
+    font: inherit;
+    font-size: 100%;
+    vertical-align: baseline;
+}
+
+.tt-menu, .ui-menu {
+    width: 100%;
+    max-width: 700px;
+    margin-top: 2px;
+    padding: 5px 0;
+    background-color: #ffffff;
+    border: 1px solid rgba(0, 0, 0, 0.15);
+    border-radius: 4px;
+    -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+    background-clip: padding-box;
+
+}
+
+.tt-suggestion, .ui-menu-item {
+    display: block;
+    padding: 6px 20px;
+}
+
+.ui-helper-hidden-accessible {
+    display: none;
+}
+
+.typeahead-footer {
+    border-top: 1px solid #eee;
+    margin-top: 10px;
+    padding: 20px;
+}
+
+.title {
+    color: #222;
+}
+
+.title strong {
+    color: #000;
+}
+
+.author {
+    color: #666
+}
+
+.author strong {
+    color: #444
+}
+
+.score {
+    color: #888;
+}
+
+td {
+    vertical-align: top;
+    text-align: left;
+}
\ No newline at end of file
diff --git a/demo/demo.html b/demo/demo.html
new file mode 100644
index 0000000..5c0dd2c
--- /dev/null
+++ b/demo/demo.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title></title>
+
+
+    <script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/typeahead.js/0.11.1/typeahead.jquery.min.js"></script>
+    <script src="../dist-browser/fuzzaldrin-plus.min.js"></script>
+
+    <link href="demo.css" rel="stylesheet" type="text/css"/>
+
+
+</head>
+<body>
+
+<h2>Source</h2>
+<textarea id="sourcetxt"></textarea>
+
+<div class="center">
+    <label>Enter one item per line then press update</label>
+    <button onclick="set_txt_source()"> Update source</button>
+</div>
+
+<h2>Make a search</h2>
+
+<div class="typeahead">
+    <input id="demo-query" class="twitter-typeahead" name="demo-query" autocomplete="off" type="text">
+</div>
+
+<!-- allow to scroll so autocomplete is at top of page -->
+<div style="height: 500px;"></div>
+
+
+<script>
+
+    var global_query = "";
+    var global_candidates = [];
+
+
+    //- - - - - - - - - - -
+    //   Typeahead Setup
+    // - - - - - - - - - - -
+    $('#demo-query').typeahead({
+                minLength: 2,
+                highlight: false //let FuzzySearch handle highlight
+            },
+            {
+                name: 'filtered',
+                limit: 10,
+
+                source: function (query, callback) {
+                    global_query = query;
+                    callback(fuzzaldrin.filter(global_candidates, query))
+                },
+
+                templates: {
+                    suggestion: function (result) {
+                        return "<div>" + fuzzaldrin.wrap(result, global_query) + "</div>"
+                    }
+                }
+
+            });
+
+
+    // - - - - - - - - - - - -
+    //   Setup dataset
+    //- - - - - - - - - - - - -
+
+    $.ajaxSetup({cache: true});
+    function set_json_source(url) {
+        $.getJSON(url).then(function (response) {
+            global_candidates = response;
+            $("#sourcetxt").val(response.join("\n"))
+        });
+    }
+    // Load movie dataset, default options are good for list of string.
+    set_json_source("movies.json");
+
+    function set_txt_source() {
+        global_candidates = $("#sourcetxt").val().split("\n")
+    }
+
+
+</script>
+
+
+</body>
+</html>
\ No newline at end of file
diff --git a/demo/movies.json b/demo/movies.json
new file mode 100644
index 0000000..7986534
--- /dev/null
+++ b/demo/movies.json
@@ -0,0 +1,1005 @@
+["A Nous la Liberte (1932)",
+  "About Schmidt (2002)",
+  "Absence of Malice (1981)",
+  "Adam's Rib (1949)",
+  "Adaptation (2002)",
+  "The Adjuster (1991)",
+  "The Adventures of Robin Hood (1938)",
+  "Affliction (1998)",
+  "The African Queen (1952)",
+  "L'Age d'Or (1930, reviewed 1964)",
+  "Aguirre, the Wrath of God (1972, reviewed 1977)",
+  "A.I. (2001)",
+  "Airplane! (1980)",
+  "Aladdin (1992)",
+  "Alexander Nevsky (1939)",
+  "Alice Doesn't Live Here Anymore (1975)",
+  "Alice's Restaurant (1969)",
+  "Aliens (1986)",
+  "All About Eve (1950)",
+  "All About My Mother (1999)",
+  "All Quiet on the Western Front (1930)",
+  "All That Heaven Allows (1956)",
+  "All the King's Men (1949)",
+  "All the President's Men (1976)",
+  "Amadeus (1984)",
+  "Amarcord (1974)",
+  "Amélie (2001)",
+  "America, America (1963)",
+  "The American Friend (1977)",
+  "American Graffiti (1973)",
+  "An American in Paris (1951)",
+  "The Americanization of Emily (1964)",
+  "American Movie (1999)",
+  "Amores Perros (2000)",
+  "Anastasia (1956)",
+  "Anatomy of a Murder (1959)",
+  "The Angry Silence (1960)",
+  "Anna and the King of Siam (1946)",
+  "Anna Christie (1930)",
+  "Annie Hall (1977)",
+  "The Apartment (1960)",
+  "Apocalypse Now (1979)",
+  "Apollo 13 (1995)",
+  "The Apostle (1997)",
+  "L'Argent (1983)",
+  "Ashes and Diamonds (1958, reviewed 1961)",
+  "Ashes and Diamonds (1958)",
+  "The Asphalt Jungle (1950)",
+  "L'Atalante (1934, reviewed 1947)",
+  "Atlantic City (1981)",
+  "Au Revoir Les Enfants (1988)",
+  "L'Avventura (1961)",
+  "The Awful Truth (1937)",
+  "Babette's Feast (1987)",
+  "Baby Doll (1956)",
+  "Back to the Future (1985)",
+  "The Bad and the Beautiful (1953)",
+  "Bad Day at Black Rock (1955)",
+  "Badlands (1973)",
+  "The Baker's Wife (1940)",
+  "Ball of Fire (1942)",
+  "The Ballad of Cable Hogue (1970)",
+  "Bambi (1942)",
+  "The Band Wagon (1953)",
+  "Bang the Drum Slowly (1973)",
+  "The Bank Dick (1940)",
+  "Barfly (1987)",
+  "Barry Lyndon (1975)",
+  "Barton Fink (1991)",
+  "The Battle of Algiers (1965, reviewed 1967)",
+  "Le Beau Mariage (1982)",
+  "Beautiful People (2000)",
+  "Beauty and the Beast (1947)",
+  "Beauty and the Beast (1991)",
+  "Bed and Board (1971)",
+  "Beetlejuice (1988)",
+  "Before Night Falls (2000)",
+  "Before the Rain (1994, reviewed 1995)",
+  "Being John Malkovich (1999)",
+  "Being There (1979)",
+  "Belle de Jour (1968)",
+  "Ben-Hur (1959)",
+  "Berlin Alexanderplatz (1983)",
+  "The Best Years of Our Lives (1946)",
+  "Beverly Hills Cop (1984)",
+  "The Bicycle Thief (1949)",
+  "The Big Chill (1983)",
+  "The Big Clock (1948)",
+  "The Big Deal on Madonna Street (1960)",
+  "The Big Heat (1953)",
+  "Big Night (1996)",
+  "The Big Red One (1980)",
+  "The Big Sky (1952)",
+  "The Big Sleep (1946)",
+  "Billy Liar (1963)",
+  "Biloxi Blues (1988)",
+  "The Birds (1963)",
+  "Birdy (1984)",
+  "Black Narcissus (1947)",
+  "Black Orpheus (1959)",
+  "Black Robe (1991)",
+  "Blazing Saddles (1974)",
+  "Bloody Sunday (2002)",
+  "Blow-Up (1966)",
+  "Blue Collar (1978)",
+  "Blue Velvet (1986)",
+  "Bob & Carol & Ted & Alice (1969)",
+  "Bob le Flambeur (1955, reviewed 1981)",
+  "Body Heat (1981)",
+  "Bonnie and Clyde (1967)",
+  "Boogie Nights (1997)",
+  "Born on the Fourth of July (1989)",
+  "Born Yesterday (1950)",
+  "Le Boucher (1970)",
+  "Bound for Glory (1976)",
+  "Boys Don't Cry (1999)",
+  "Boyz N the Hood (1991)",
+  "Brazil (1985)",
+  "Bread, Love and Dreams (1954)",
+  "Breaker Morant (1980)",
+  "The Breakfast Club (1985)",
+  "Breaking Away (1979)",
+  "Breaking the Waves (1996)",
+  "Breathless (1961)",
+  "The Bride Wore Black (1968)",
+  "The Bridge on the River Kwai (1957)",
+  "Brief Encounter (1946)",
+  "A Brief History of Time (1992)",
+  "Bringing Up Baby (1938)",
+  "Broadcast News (1987)",
+  "Brother's Keeper (1992)",
+  "The Buddy Holly Story (1978)",
+  "Bull Durham (1988)",
+  "Bullitt (1968)",
+  "Bus Stop (1956)",
+  "Butch Cassidy and the Sundance Kid (1969)",
+  "The Butcher Boy (1998)",
+  "Bye Bye Brasil (1980)",
+  "The Earrings of Madame De . . . (1954)",
+  "Cabaret (1972)",
+  "The Caine Mutiny (1954)",
+  "California Suite (1978)",
+  "Calle 54 (2000)",
+  "Camelot (1967)",
+  "Camille (1937)",
+  "Captains Courageous (1937)",
+  "Carmen Jones (1954)",
+  "Carnal Knowledge (1971)",
+  "Casablanca (1942)",
+  "Cat on a Hot Tin Roof (1958)",
+  "Catch-22 (1970)",
+  "Cavalcade (1933)",
+  "The Celebration (1998)",
+  "La Cérémonie (1996)",
+  "Chan Is Missing (1982)",
+  "Chariots of Fire (1981)",
+  "Charley Varrick (1973)",
+  "Chicago (2002)",
+  "Chicken Run (2000)",
+  "La Chienne (1931, reviewed 1975)",
+  "Chinatown (1974)",
+  "Chloë in the Afternoon (1972)",
+  "Chocolat (1988, reviewed 1989)",
+  "The Cider House Rules (1999)",
+  "The Citadel (1938)",
+  "Citizen Kane (1941)",
+  "Claire's Knee (1971)",
+  "The Clockmaker (1973, reviewed 1976)",
+  "A Clockwork Orange (1971)",
+  "Close Encounters of the Third Kind (1977)",
+  "Close-Up (1990, reviewed 1999)",
+  "Clueless (1995)",
+  "Coal Miner's Daughter (1980)",
+  "The Color of Money (1986)",
+  "Come Back, Little Sheba (1952)",
+  "Coming Home (1978)",
+  "The Conformist (1970)",
+  "The Conquest of Everest (1953)",
+  "Contempt (1964)",
+  "The Conversation (1974)",
+  "Cool Hand Luke (1967)",
+  "The Count of Monte Cristo (1934)",
+  "The Country Girl (1954)",
+  "The Cousins (1959)",
+  "The Cranes Are Flying (1960)",
+  "Cries and Whispers (1972)",
+  "Crossfire (1947)",
+  "Crumb (1994)",
+  "Cry, the Beloved Country (1952)",
+  "The Crying Game (1992)",
+  "Damn Yankees (1958)",
+  "The Damned (1969)",
+  "Dance with a Stranger (1985)",
+  "Dangerous Liaisons (1988)",
+  "Daniel (1983)",
+  "Danton (1983)",
+  "Dark Eyes (1987)",
+  "Dark Victory (1939)",
+  "Darling (1965)",
+  "David Copperfield (1935)",
+  "David Holtzman's Diary (1968, reviewed 1973)",
+  "Dawn of the Dead (1979)",
+  "Day for Night (1973)",
+  "The Day of the Jackal (1973)",
+  "The Day the Earth Stood Still (1951)",
+  "Days of Heaven (1978)",
+  "Days of Wine and Roses (1963)",
+  "The Dead (1987)",
+  "Dead Calm (1989)",
+  "Dead End (1937)",
+  "Dead Man Walking (1995)",
+  "Dead of Night (1946, reviewed 1946)",
+  "Dead Ringers (1988)",
+  "Death in Venice (1971)",
+  "Death of a Salesman (1951)",
+  "The Decalogue (2000)",
+  "Deep End (1971)",
+  "The Deer Hunter (1978)",
+  "The Defiant Ones (1958)",
+  "Deliverance (1972)",
+  "Desperately Seeking Susan (1985)",
+  "Destry Rides Again (1939)",
+  "Diabolique (1955)",
+  "Dial M for Murder (1954)",
+  "Diary of a Chambermaid (1964)",
+  "Diary of a Country Priest (1950, reviewed 1954)",
+  "Die Hard (1988)",
+  "Diner (1982)",
+  "Dinner at Eight (1933)",
+  "The Dirty Dozen (1967)",
+  "Dirty Harry (1971)",
+  "Dirty Rotten Scoundrels (1988)",
+  "The Discreet Charm of the Bourgeoisie (1972)",
+  "Disraeli (1929)",
+  "Distant Thunder (1973)",
+  "Diva (1982)",
+  "Divorce-Italian Style (1962)",
+  "Do the Right Thing (1989)",
+  "Dr. Jekyll and Mr. Hyde (1932)",
+  "Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb (1964)",
+  "Doctor Zhivago (1965)",
+  "Dodsworth (1936)",
+  "La Dolce Vita (1961)",
+  "Donnie Brasco (1997)",
+  "Don't Look Back (1967)",
+  "Double Indemnity (1944)",
+  "Down by Law (1986)",
+  "Dracula (1931)",
+  "The Dreamlife of Angels (1998)",
+  "Dressed to Kill (1980)",
+  "The Dresser (1983)",
+  "Driving Miss Daisy (1989)",
+  "Drowning by Numbers (1991)",
+  "Drugstore Cowboy (1989)",
+  "Duck Soup (1933)",
+  "The Duellists (1978)",
+  "Dumbo (1941)",
+  "The Earrings of Madame De . . .",
+  "East of Eden (1955)",
+  "Easy Living (1937)",
+  "Eat Drink Man Woman (1994)",
+  "Effi Briest (1977)",
+  "8 1/2 (1963)",
+  "Eight Men Out (1988)",
+  "The Elephant Man (1980)",
+  "Elmer Gantry (1960)",
+  "Empire of the Sun (1987)",
+  "Enemies, A Love Story (1989)",
+  "Les Enfants du Paradis (1945, reviewed 1947)",
+  "The English Patient (1996)",
+  "The Entertainer (1960)",
+  "Entre Nous (1983)",
+  "E.T. the Extra-Terrestrial (1982)",
+  "Europa, Europa (1991)",
+  "Every Man for Himself (1980)",
+  "The Exorcist (1973)",
+  "The Exterminating Angel (1967)",
+  "A Face in the Crowd (1957)",
+  "Face to Face (1976)",
+  "Faces (1968)",
+  "The Family Game (1984)",
+  "Fanny & Alexander (1983)",
+  "Fantasia (1940)",
+  "Farewell, My Concubine (1993)",
+  "Far from Heaven (2002)",
+  "Fargo (1996)",
+  "Fast, Cheap & Out of Control (1997)",
+  "Fast Runner (Atanarjuat) (2002)",
+  "Fat City (1972)",
+  "Fatal Attraction (1987)",
+  "Father of the Bride (1950)",
+  "Fellini Satyricon (1970)",
+  "La Femme Infidèle (1969)",
+  "La Femme Nikita (1991)",
+  "The Fisher King (1991)",
+  "Fist in His Pocket (1968)",
+  "Fitzcarraldo (1982)",
+  "Five Easy Pieces (1970)",
+  "The Flamingo Kid (1984)",
+  "The Fly (1958)",
+  "The Flamingo Kid (1984)",
+  "Force of Evil (1948)",
+  "For Whom the Bell Tolls (1943)",
+  "Forbidden Games (1952)",
+  "A Foreign Affair (1948)",
+  "The Fortune Cookie (1966)",
+  "The 400 Blows (1959)",
+  "Frankenstein (1931)",
+  "The French Connection (1971)",
+  "Frenzy (1972)",
+  "Friendly Persuasion (1956)",
+  "From Here to Eternity (1953)",
+  "The Fugitive (1947)",
+  "Full Metal Jacket (1987)",
+  "The Full Monty (1997)",
+  "Funny Face (1957)",
+  "Funny Girl (1968)",
+  "Fury (1936)",
+  "Gallipoli (1981)",
+  "Gandhi (1982)",
+  "Gangs of New York (2002)",
+  "The Garden of the Finzi-Continis (1971)",
+  "Gas Food Lodging (1992)",
+  "Gaslight (1944)",
+  "Gate of Hell (1954)",
+  "A Geisha (1978)",
+  "The General (1998)",
+  "General Della Rovere (1960)",
+  "Genevieve (1954)",
+  "Gentlemen Prefer Blondes (1953)",
+  "Georgy Girl (1966)",
+  "Get Carter (1971)",
+  "Get Out Your Handkerchiefs (1978)",
+  "Ghost World (2001)",
+  "Giant (1956)",
+  "Gigi (1958)",
+  "Gimme Shelter (1970)",
+  "The Girl Can't Help It (1956)",
+  "Girl with a Suitcase (1961)",
+  "The Gleaners and I (2001)",
+  "The Goalie's Anxiety at the Penalty Kick (1977)",
+  "The Go-Between (1971)",
+  "The Godfather (1972)",
+  "The Godfather Part II (1974)",
+  "Going My Way (1944)",
+  "Goldfinger (1964)",
+  "Gone With the Wind (1939)",
+  "The Good, the Bad and the Ugly (1968)",
+  "The Good Earth (1937)",
+  "Goodbye, Mr. Chips (1939)",
+  "GoodFellas (1990)",
+  "Gosford Park (2001)",
+  "The Graduate (1967)",
+  "Grand Hotel (1932)",
+  "Grand Illusion (1938)",
+  "The Grapes of Wrath (1940)",
+  "The Great Dictator (1940)",
+  "Great Expectations (1947)",
+  "The Great Man (1957)",
+  "The Great McGinty (1940)",
+  "The Greatest Show on Earth (1952)",
+  "Green for Danger (1947)",
+  "Gregory's Girl (1982)",
+  "The Grifters (1990)",
+  "Groundhog Day (1993)",
+  "The Gunfighter (1950)",
+  "Gunga Din (1939)",
+  "Hail the Conquering Hero (1944)",
+  "Hair (1979)",
+  "Hamlet (1948)",
+  "Hamlet (2000)",
+  "Handle With Care (1977)",
+  "Hannah and Her Sisters (1986)",
+  "Happiness (1998)",
+  "A Hard Day's Night (1964)",
+  "Harlan County, USA (1976)",
+  "Harry and Tonto (1974)",
+  "A Hatful of Rain (1957)",
+  "The Heartbreak Kid (1972)",
+  "Heartland (1981)",
+  "Hearts of Darkness: A Filmmaker's Apocalypse (1991)",
+  "Heat and Dust (1983)",
+  "Heathers (1989)",
+  "Heavy Traffic (1973)",
+  "Heimat (1985)",
+  "The Heiress (1949)",
+  "Henry V (1946)",
+  "Henry V (1989)",
+  "Henry Fool (1998)",
+  "Here Comes Mr. Jordan (1941)",
+  "High and Low (Japan) (1963)",
+  "The High and the Mighty (1954)",
+  "High Art (1998)",
+  "High Hopes (1988)",
+  "High Noon (1952)",
+  "High Sierra (1941)",
+  "The Hill (1965)",
+  "Hiroshima Mon Amour (1960)",
+  "His Girl Friday (1940)",
+  "The Homecoming (1973)",
+  "Hoop Dreams (1994)",
+  "Hope and Glory (1987)",
+  "Hotel Terminus: Klaus Barbie et son Temps (1988)",
+  "The Hours (2002)",
+  "Household Saints (1993)",
+  "House of Games (1987)",
+  "How Green Was My Valley (1941)",
+  "How to Marry a Millionaire (1953)",
+  "Howards End (1992)",
+  "Hud (1963)",
+  "Ken Burns' America: Huey Long (1985)",
+  "Husbands and Wives (1992)",
+  "The Hustler (1961)",
+  "I Know Where I'm Going! (1947)",
+  "I Remember Mama (1948)",
+  "I Want to Live! (1958)",
+  "If... (1969)",
+  "Ikiru (1952, reviewed 1960)",
+  "I'm All Right Jack (1960)",
+  "Imitation of Life (1959)",
+  "In Cold Blood (1967)",
+  "In the Bedroom (2001)",
+  "In the Heat of the Night (1967)",
+  "The Informer (1935)",
+  "Inherit the Wind (1960)",
+  "The Insider (1999)",
+  "Internal Affairs (1990)",
+  "The Ipcress File (1965)",
+  "It Happened One Night (1934)",
+  "It's a Gift (1935)",
+  "It's a Wonderful Life (1946)",
+  "Jailhouse Rock (1957)",
+  "Jaws (1975)",
+  "The Jazz Singer (1927)",
+  "Jean de Florette (1987)",
+  "Jerry Maguire (1996)",
+  "Johnny Guitar (1954)",
+  "The Judge and the Assassin (1982)",
+  "Judgment at Nuremberg (1961)",
+  "Ju Dou (1990)",
+  "Jules and Jim (1962)",
+  "Juliet of the Spirits (1965)",
+  "Junior Bonner (1972)",
+  "Kagemusha (1980)",
+  "The Killers (1946)",
+  "The Killing Fields (1984)",
+  "Kind Hearts and Coronets (1950)",
+  "The King and I (1956)",
+  "King Kong (1933)",
+  "King Lear (1971)",
+  "The King of Comedy (1983)",
+  "The King of Marvin Gardens (1972)",
+  "Kiss of the Spider Woman (1985)",
+  "Klute (1971)",
+  "Knife in the Water (1963)",
+  "Kramer vs. Kramer (1979)",
+  "L.A. Confidential (1997)",
+  "Lacombe Lucien (1974)",
+  "The Lady Eve (1941)",
+  "The Lady Vanishes (1938)",
+  "Ladybird, Ladybird (1994)",
+  "Lamerica (1994, reviewed 1995)",
+  "The Last American Hero (1973)",
+  "The Last Emperor (1987)",
+  "The Last Metro (1980)",
+  "The Last Picture Show (1971)",
+  "The Last Seduction (1994)",
+  "Last Tango in Paris (1973)",
+  "The Last Temptation of Christ (1988)",
+  "The Last Waltz (1978)",
+  "Laura (1944)",
+  "The Lavender Hill Mob (1951)",
+  "Lawrence of Arabia (1962)",
+  "A League of Their Own (1992)",
+  "Leaving Las Vegas (1995)",
+  "The Leopard (1963)",
+  "The Letter (1963)",
+  "A Letter to Three Wives (1949)",
+  "Les Liaisons Dangereuses 1960 (1961)",
+  "The Life and Death of Colonel Blimp (1945)",
+  "Life Is Sweet (1991)",
+  "The Life of Emile Zola (1937)",
+  "Life With Father (1947)",
+  "Like Water for Chocolate (1992, reviewed 1993)",
+  "Lili (1953)",
+  "Little Big Man (1970)",
+  "Little Caesar (1931)",
+  "The Little Foxes (1941)",
+  "The Little Fugitive (1953)",
+  "The Little Kidnappers (1954)",
+  "Little Vera (1988, reviewed 1989)",
+  "Little Women (1933)",
+  "Little Women (1994)",
+  "The Lives of a Bengal Lancer (1935)",
+  "Living in Oblivion (1995)",
+  "Local Hero (1983)",
+  "Lola (1982)",
+  "Lola Montès (1968)",
+  "Lolita (1962)",
+  "Lone Star (1996)",
+  "The Loneliness of the Long Distance Runner (1962)",
+  "Long Day's Journey into Night (1962)",
+  "The Long Goodbye (1973)",
+  "The Long Good Friday (1982)",
+  "The Long Voyage Home (1940)",
+  "The Longest Day (1962)",
+  "Look Back in Anger (1959)",
+  "Lost Horizon (1937)",
+  "Lost in America (1985)",
+  "The Lost Weekend (1945)",
+  "Love (1973)",
+  "Love Affair (1939)",
+  "Love and Death (1975)",
+  "A Love in Germany (1984)",
+  "Love in the Afternoon (1957)",
+  "Lovely and Amazing (2002)",
+  "Love on the Run (1979)",
+  "Lover Come Back (1962)",
+  "The Lovers (1959)",
+  "Loves of a Blonde (1966)",
+  "Loving (1970)",
+  "Lust for Life (1956)",
+  "M (1931, reviewed 1933)",
+  "Mad Max (1980)",
+  "The Madness of King George (1994)",
+  "The Magic Flute (1975)",
+  "The Major and the Minor (1942)",
+  "Major Barbara (1941)",
+  "Make Way for Tomorrow (1937)",
+  "Malcolm X (1992)",
+  "The Maltese Falcon (1941)",
+  "A Man for All Seasons (1966)",
+  "Man Hunt (1941)",
+  "The Man Who Came to Dinner (1942)",
+  "The Man Who Loved Women (1977)",
+  "The Man Who Wasn't There (2001)",
+  "The Man With the Golden Arm (1955)",
+  "The Manchurian Candidate (1962)",
+  "Manhattan (1979)",
+  "Manon of the Spring (1987)",
+  "Marriage Italian Style (1964)",
+  "The Marriage of Maria Braun (1979)",
+  "Married to the Mob (1988)",
+  "The Marrying Kind (1952)",
+  "Marty (1955)",
+  "Mary Poppins (1964)",
+  "M*A*S*H (1970)",
+  "The Match Factory Girl (1990)",
+  "Mayerling (1937)",
+  "McCabe & Mrs. Miller (1971)",
+  "Mean Streets (1973)",
+  "Meet Me in St. Louis (1944)",
+  "Melvin and Howard (1980)",
+  "Memories of Underdevelopment (1973)",
+  "The Memory of Justice (1976)",
+  "The Men (1950)",
+  "Ménage (1986)",
+  "Metropolitan (1990)",
+  "Midnight (1939)",
+  "Midnight Cowboy (1969)",
+  "Minnie and Moskowitz (1971)",
+  "The Miracle of Morgan's Creek (1944)",
+  "Miracle on 34th Street (1947)",
+  "The Miracle Worker (1962)",
+  "Les Miserables (1935)",
+  "The Misfits (1961)",
+  "Missing (1982)",
+  "Mr. and Mrs. Bridge (1990)",
+  "Mr. Deeds Goes to Town (1936)",
+  "Mr. Hulot's Holiday (1954)",
+  "Mister Roberts (1955)",
+  "Mr. Smith Goes to Washington (1939)",
+  "Mrs. Miniver (1942)",
+  "Mon Oncle d'Amérique (1980)",
+  "Mona Lisa (1986)",
+  "Monsieur Verdoux (1947, reviewed 1964)",
+  "Monsters, Inc. (2001)",
+  "Moonlighting (1982)",
+  "Moonstruck (1987)",
+  "The More the Merrier (1943)",
+  "Morgan! (1966)",
+  "The Mortal Storm (1940)",
+  "Mother (1996)",
+  "Moulin Rouge (1953)",
+  "The Mouthpiece (1932)",
+  "Much Ado About Nothing (1993)",
+  "Mulholland Dr. (2001)",
+  "Murmur of the Heart (1971)",
+  "Mutiny on the Bounty (1935)",
+  "My Beautiful Laundrette (1986)",
+  "My Darling Clementine (1946)",
+  "My Dinner With Andre (1981)",
+  "My Fair Lady (1964)",
+  "My Left Foot (1989)",
+  "My Life as a Dog (1987)",
+  "My Man Godfrey (1936)",
+  "My Night at Maud's (1969)",
+  "My Own Private Idaho (1991)",
+  "My 20th Century (1990)",
+  "Mon Oncle (1958)",
+  "The Naked Gun: From the Files of Police Squad! (1988)",
+  "Nashville (1975)",
+  "National Lampoon's Animal House (1978)",
+  "National Velvet (1944)",
+  "Network (1976)",
+  "Never on Sunday (1960)",
+  "Night Moves (1975)",
+  "The Night of the Hunter (1955)",
+  "Night of the Living Dead (1968)",
+  "A Night to Remember (1958)",
+  "A Nightmare on Elm Street (1984)",
+  "1900 (1977)",
+  "Ninotchka (1939)",
+  "Nobody's Fool (1994)",
+  "Norma Rae (1979)",
+  "North by Northwest (1959)",
+  "Nothing But the Best (1964)",
+  "Notorious (1946)",
+  "Now, Voyager (1942)",
+  "La Nuit De Varennes (1983)",
+  "The Nun's Story (1959)",
+  "Odd Man Out (1947)",
+  "Of Mice and Men (1940)",
+  "Oklahoma! (1955)",
+  "Oliver Twist (1951)",
+  "Los Olvidados (1950, reviewed 1952)",
+  "On the Beach (1959)",
+  "On the Town (1949)",
+  "On the Waterfront (1954)",
+  "One False Move (1992)",
+  "One Flew Over the Cuckoo's Nest (1975)",
+  "One Foot in Heaven (1941)",
+  "One Hour with You (1932)",
+  "One Night of Love (1934)",
+  "One Potato, Two Potato (1964)",
+  "One, Two, Three (1961)",
+  "Only Angels Have Wings (1939)",
+  "Open City (1946)",
+  "Operation Crossbow (1965)",
+  "The Opposite of Sex (1998)",
+  "Ordinary People (1980)",
+  "Ossessione (1942, reviewed 1976)",
+  "Othello (1952, reviewed 1955)",
+  "Our Town (1940)",
+  "Out of the Past (1947)",
+  "The Outlaw Josey Wales (1976)",
+  "The Overlanders (1946)",
+  "The Ox-Bow Incident (1943)",
+  "Paint Your Wagon (1969)",
+  "Paisan (1948)",
+  "The Palm Beach Story (1942)",
+  "The Parallax View (1974)",
+  "A Passage to India (1984)",
+  "The Passion of Anna (1970)",
+  "Pather Panchali (1958)",
+  "Paths of Glory (1957)",
+  "Patton (1970)",
+  "The Pawnbroker (1965)",
+  "Payday (1973)",
+  "Pelle the Conqueror (1988)",
+  "The People Vs. Larry Flynt (1996)",
+  "Persona (1967)",
+  "Persuasion (1995)",
+  "Le Petit Theatre de Jean Renoir (1974)",
+  "Petulia (1968)",
+  "The Philadelphia Story (1940)",
+  "The Pianist (2002)",
+  "The Piano (1993)",
+  "Pickup on South Street (1953)",
+  "The Pillow Book (1997)",
+  "Pillow Talk (1959)",
+  "The Pink Panther (1964)",
+  "Pinocchio (1940)",
+  "Pixote (1981)",
+  "A Place in the Sun (1951)",
+  "Places in the Heart (1984)",
+  "Platoon (1986)",
+  "Play Misty for Me (1971)",
+  "The Player (1992)",
+  "Playtime (1967, reviewed 1973)",
+  "Point Blank (1967)",
+  "Poltergeist (1982)",
+  "Ponette (1997)",
+  "Il Postino (The Postman) (1994)",
+  "The Postman Always Rings Twice (1946)",
+  "Pretty Baby (1978)",
+  "Pride and Prejudice (1940)",
+  "The Pride of the Yankees (1942)",
+  "Prince of the City (1981)",
+  "The Prisoner (1955)",
+  "The Private Life of Henry VIII (1933)",
+  "Prizzi's Honor (1985)",
+  "The Producers (1968)",
+  "Psycho (1960)",
+  "The Public Enemy (1931)",
+  "Pulp Fiction (1994)",
+  "The Purple Rose of Cairo (1985)",
+  "Pygmalion (1938)",
+  "Quadrophenia (1979)",
+  "The Quiet Man (1952)",
+  "Raging Bull (1980)",
+  "Raiders of the Lost Ark (1981)",
+  "Rain Man (1988)",
+  "Raise the Red Lantern (1991, reviewed 1992)",
+  "Raising Arizona (1987)",
+  "Ran (1985)",
+  "The Rapture (1991)",
+  "Rashomon (1951)",
+  "Re-Animator (1985)",
+  "Rear Window (1954)",
+  "Rebecca (1940)",
+  "Rebel Without a Cause (1955)",
+  "Red (1994)",
+  "The Red Badge of Courage (1951)",
+  "Red River (1948)",
+  "The Red Shoes (1948)",
+  "Reds (1981)",
+  "The Remains of the Day (1993)",
+  "Repo Man (1984)",
+  "Repulsion (1965)",
+  "Reservoir Dogs (1992)",
+  "The Return of Martin Guerre (1983)",
+  "Reuben, Reuben (1983)",
+  "Reversal of Fortune (1990)",
+  "Richard III (1956)",
+  "Ride the High Country (1962)",
+  "Rififi (1956)",
+  "The Right Stuff (1983)",
+  "Risky Business (1983)",
+  "River's Edge (1987)",
+  "The Road Warrior (1982)",
+  "Robocop (1987)",
+  "Rocco and His Brothers (1960, reviewed 1961)",
+  "Roger & Me (1989)",
+  "Roman Holiday (1953)",
+  "Romeo and Juliet (1936)",
+  "Romeo and Juliet (1968)",
+  "Room at the Top (1959)",
+  "A Room With a View (1986)",
+  "The Rose Tattoo (1955)",
+  "Rosemary's Baby (1968)",
+  "'Round Midnight (1986)",
+  "Ruggles of Red Gap (1935)",
+  "The Rules of the Game (1939, reviewed 1950 and 1961)",
+  "The Ruling Class (1972)",
+  "Rushmore (1998)",
+  "Ruthless People (1986)",
+  "Sahara (1943)",
+  "Salaam Bombay! (1988)",
+  "Salesman (1969)",
+  "Sanjuro (1963)",
+  "Sansho the Bailiff (1969)",
+  "Saturday Night and Sunday Morning (1961)",
+  "Saturday Night Fever (1977)",
+  "Saving Private Ryan (1998)",
+  "Say Anything... (1989)",
+  "Sayonara (1957)",
+  "Scenes From a Marriage (1974)",
+  "Schindler's List (1993)",
+  "The Scoundrel (1935)",
+  "The Search (1948)",
+  "The Searchers (1956)",
+  "Secret Honor (1985)",
+  "Secrets and Lies (1996)",
+  "Sense and Sensibility (1995)",
+  "Sergeant York (1941)",
+  "Serpico (1973)",
+  "The Servant (1963, reviewed 1964)",
+  "The Set-Up (1949)",
+  "Seven Beauties (1976)",
+  "Seven Brides for Seven Brothers (1954)",
+  "Seven Days to Noon (1950)",
+  "The Seven Samurai (1956)",
+  "7 Up/28 Up (1985)",
+  "The Seven Year Itch (1955)",
+  "The Seventh Seal (1958)",
+  "Sex, Lies and Videotape (1989)",
+  "Sexy Beast (2001)",
+  "Shadow of a Doubt (1943)",
+  "Shaft (1971)",
+  "Shakespeare in Love (1998)",
+  "Shane (1953)",
+  "She Wore a Yellow Ribbon (1949)",
+  "Sherman's March (1986)",
+  "She's Gotta Have It (1986)",
+  "The Shining (1980)",
+  "Ship of Fools (1965)",
+  "Shoah (1985)",
+  "Shock Corridor (1963)",
+  "Shoeshine (1947)",
+  "Shoot the Piano Player (1962)",
+  "The Shooting Party (1985)",
+  "The Shootist (1976)",
+  "The Shop Around the Corner (1940)",
+  "The Shop on Main Street (1966)",
+  "A Shot in the Dark (1964)",
+  "Shrek (2001)",
+  "Sid and Nancy (1986)",
+  "The Silence (1964)",
+  "The Silence of the Lambs (1991)",
+  "The Silent World (1956)",
+  "Silk Stockings (1957)",
+  "Silkwood (1983)",
+  "Singin' in the Rain (1952)",
+  "Sitting Pretty (1948)",
+  "Sleeper (1973)",
+  "A Slight Case of Murder (1938)",
+  "Smash Palace (1982)",
+  "Smile (1975)",
+  "Smiles of a Summer Night (1956, reviewed 1957)",
+  "The Snake Pit (1948)",
+  "Snow White and the Seven Dwarfs (1938)",
+  "Some Like It Hot (1959)",
+  "The Sorrow and the Pity (Le Chagrin et la Pitié) (1971)",
+  "The Sound of Music (1965)",
+  "South Pacific (1958)",
+  "Spartacus (1960)",
+  "Spellbound (1945)",
+  "The Spiral Staircase (1946)",
+  "Spirited Away (2002)",
+  "Splendor in the Grass (1961)",
+  "Stage Door (1937)",
+  "Stagecoach (1939)",
+  "Stairway to Heaven (1946)",
+  "Stalag 17 (1953)",
+  "A Star Is Born (1937)",
+  "Star Trek II: The Wrath of Khan (1982)",
+  "Star Wars (1977)",
+  "Starman (1984)",
+  "The Stars Look Down (1941)",
+  "State Fair (1933)",
+  "Stevie (1981)",
+  "Stolen Kisses (1969)",
+  "Stop Making Sense (1984)",
+  "Stormy Monday (1988)",
+  "The Story of Adèle H. (1975)",
+  "The Story of G.I. Joe (1945)",
+  "The Story of Qiu Ju (1992)",
+  "Story of Women (1989)",
+  "Storytelling (2001)",
+  "La Strada (1956)",
+  "The Straight Story (1999)",
+  "Straight Time (1978)",
+  "Stranger Than Paradise (1984)",
+  "Strangers on a Train (1951)",
+  "Straw Dogs (1971)",
+  "A Streetcar Named Desire (1951)",
+  "Stroszek (1977)",
+  "Suddenly, Last Summer (1959)",
+  "The Sugarland Express (1974)",
+  "Sullivan's Travels (1941)",
+  "Summer (1986)",
+  "Summertime (1955)",
+  "Sunday Bloody Sunday (1971)",
+  "Sundays and Cybele (1962)",
+  "Sunset Boulevard (1950)",
+  "Suspicion (1941)",
+  "The Sweet Hereafter (1997)",
+  "Sweet Smell of Success (1957)",
+  "Sweet Sweetback's Baadasssss Song (1971)",
+  "Swept Away (By an Unusual Destiny in the Blue Sea of August) (1974)",
+  "Swing Time (1936)",
+  "The Taking of Pelham One Two Three (1974)",
+  "Talk to Her (2002)",
+  "Tampopo (1986)",
+  "Taste of Cherry (1997)",
+  "A Taste of Honey (1961, reviewed 1962)",
+  "Taxi Driver (1976)",
+  "A Taxing Woman (1987)",
+  "A Taxing Woman's Return (1988)",
+  "Tell Them Willie Boy Is Here (1969)",
+  "10 (1979)",
+  "The Ten Commandments (1956)",
+  "Tender Mercies (1983)",
+  "The Tender Trap (1955)",
+  "Terms of Endearment (1983)",
+  "La Terra trema (1947, reviewed 1965)",
+  "Tess (1980)",
+  "That Obscure Object of Desire (1977)",
+  "That's Life! (1986)",
+  "Thelma & Louise (1991)",
+  "These Three (1936)",
+  "They Live by Night (1949)",
+  "They Shoot Horses, Don't They? (1969)",
+  "They Were Expendable (1945)",
+  "They Won't Forget (1937)",
+  "The Thief of Bagdad (1940)",
+  "The Thin Blue Line (1988)",
+  "The Thin Man (1934)",
+  "The Thin Red Line (1998)",
+  "The Third Generation (1979, reviewed 1980)",
+  "The Third Man (1949)",
+  "The Thirty-Nine Steps (1935)",
+  "Thirty Two Short Films About Glenn Gould (1994)",
+  "This Is Spinal Tap (1984)",
+  "The Man Must Die (1970)",
+  "This Sporting Life (1963)",
+  "Three Comrades (1938)",
+  "Three Days of the Condor (1975)",
+  "Throne of Blood (1957)",
+  "Tight Little Island (1949)",
+  "The Tin Drum (1979)",
+  "To Be or Not to Be (1942)",
+  "To Catch a Thief (1955)",
+  "To Have and Have Not (1944)",
+  "To Kill a Mockingbird (1962)",
+  "To Live (1994)",
+  "Tokyo Story (1953)",
+  "Tom Jones (1963)",
+  "Tootsie (1982)",
+  "Top Hat (1935)",
+  "Topaz (1969)",
+  "Topkapi (1964)",
+  "Total Recall (1990)",
+  "Touch of Evil (1958)",
+  "Toy Story (1995)",
+  "Traffic (2000)",
+  "The Train (1965)",
+  "Trainspotting (1996)",
+  "The Treasure of the Sierra Madre (1948)",
+  "A Tree Grows in Brooklyn (1945)",
+  "The Tree of the Wooden Clogs (1979)",
+  "The Trip to Bountiful (1985)",
+  "Tristana (1970)",
+  "Trouble in Paradise (1932)",
+  "The Trouble with Harry (1955)",
+  "True Grit (1969)",
+  "True Love (1989)",
+  "Trust (1991)",
+  "Tunes of Glory (1960)",
+  "12 Angry Men (1957)",
+  "Twelve O'Clock High (1949)",
+  "Twentieth Century (1934)",
+  "Two English Girls (1971)",
+  "The Two of Us (1968)",
+  "2001: A Space Odyssey (1968)",
+  "Two Women (1961)",
+  "Ugetsu (1954)",
+  "Ulzana's Raid (1972)",
+  "Umberto D. (1952)",
+  "The Unbearable Lightness of Being (1988)",
+  "Unforgiven (1992)",
+  "The Usual Suspects (1995)",
+  "Vanya on 42nd Street (1994)",
+  "The Verdict (1982)",
+  "Vertigo (1958)",
+  "Videodrome (1982)",
+  "Violette Nozière (1978)",
+  "Viridiana (1962)",
+  "Viva Zapata! (1952)",
+  "The Voice of the Turtle (1947)",
+  "The Wages of Fear (1955)",
+  "Waking Life (2001)",
+  "Walkabout (1971)",
+  "A Walk in the Sun (1945)",
+  "The War Game (1966)",
+  "The War of the Roses (1989)",
+  "The Warriors (1979)",
+  "Watch on the Rhine (1943)",
+  "The Waterdance (1991)",
+  "The Way We Were (1973)",
+  "Weekend (1968)",
+  "Welcome to the Dollhouse (1996)",
+  "The Well-Digger's Daughter (1941)",
+  "West Side Story (1961)",
+  "The Whales of August (1987)",
+  "What Ever Happened to Baby Jane? (1962)",
+  "What's Eating Gilbert Grape (1993)",
+  "What's Up, Doc? (1972)",
+  "When Harry Met Sally (1989)",
+  "White Heat (1949)",
+  "Who Framed Roger Rabbit (1988)",
+  "Who's Afraid of Virginia Woolf? (1966)",
+  "The Wild Bunch (1969)",
+  "The Wild Child (1970)",
+  "Wild Reeds (1994)",
+  "Wild Strawberries (1959)",
+  "Wilson (1944)",
+  "Wings of Desire (1988)",
+  "Wise Blood (1979)",
+  "The Wizard of Oz (1939)",
+  "Woman in the Dunes (1964)",
+  "Woman of the Year (1942)",
+  "The Women (1939)",
+  "Women in Love (1970)",
+  "Women on the Verge of a Nervous Breakdown (1988)",
+  "Woodstock (1970)",
+  "Working Girl (1988)",
+  "The World of Apu (1959, reviewed 1960)",
+  "The World of Henry Orient (1964)",
+  "Written on the Wind (1956)",
+  "Wuthering Heights (1939)",
+  "Yankee Doodle Dandy (1942)",
+  "The Year of Living Dangerously (1982)",
+  "The Yearling (1983)",
+  "Yellow Submarine (1968)",
+  "Yi Yi: A One and a Two (2000)",
+  "Yojimbo (1961)",
+  "You Can Count On Me (2000)",
+  "You Only Live Once (1937)",
+  "Young Frankenstein (1974)",
+  "Young Mr. Lincoln (1939)",
+  "Y Tu Mamá También (2001)",
+  "Z (1969)",
+  "Zero for Conduct (1933)"]
\ No newline at end of file
diff --git a/dist-browser/fuzzaldrin-plus.js b/dist-browser/fuzzaldrin-plus.js
new file mode 100644
index 0000000..198ec16
--- /dev/null
+++ b/dist-browser/fuzzaldrin-plus.js
@@ -0,0 +1,914 @@
+/* fuzzaldrin-plus - v0.3.1 - @license: MIT; @author: Jean Christophe Roy; @site: https://github.com/jeancroy/fuzzaldrin-plus */
+
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fuzzaldrin = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);i [...]
+(function() {
+  var defaultPathSeparator, pluckCandidates, scorer, sortCandidates;
+
+  scorer = require('./scorer');
+
+  defaultPathSeparator = scorer.pathSeparator;
+
+  pluckCandidates = function(a) {
+    return a.candidate;
+  };
+
+  sortCandidates = function(a, b) {
+    return b.score - a.score;
+  };
+
+  module.exports = function(candidates, query, options) {
+    var allowErrors, bKey, candidate, isPath, key, maxInners, maxResults, optCharRegEx, pathSeparator, prepQuery, score, scoredCandidates, spotLeft, string, useExtensionBonus, _i, _len;
+    if (options == null) {
+      options = {};
+    }
+    scoredCandidates = [];
+    key = options.key, maxResults = options.maxResults, maxInners = options.maxInners, allowErrors = options.allowErrors, isPath = options.isPath, useExtensionBonus = options.useExtensionBonus, optCharRegEx = options.optCharRegEx, pathSeparator = options.pathSeparator;
+    spotLeft = (maxInners != null) && maxInners > 0 ? maxInners : candidates.length;
+    bKey = key != null;
+    prepQuery = scorer.prepQuery(query, options);
+    for (_i = 0, _len = candidates.length; _i < _len; _i++) {
+      candidate = candidates[_i];
+      string = bKey ? candidate[key] : candidate;
+      if (!string) {
+        continue;
+      }
+      score = scorer.score(string, query, prepQuery, allowErrors, isPath, useExtensionBonus, pathSeparator);
+      if (score > 0) {
+        scoredCandidates.push({
+          candidate: candidate,
+          score: score
+        });
+        if (!--spotLeft) {
+          break;
+        }
+      }
+    }
+    scoredCandidates.sort(sortCandidates);
+    candidates = scoredCandidates.map(pluckCandidates);
+    if (maxResults != null) {
+      candidates = candidates.slice(0, maxResults);
+    }
+    return candidates;
+  };
+
+}).call(this);
+
+},{"./scorer":4}],2:[function(require,module,exports){
+(function() {
+  var filter, matcher, parseOptions, prepQueryCache, scorer;
+
+  scorer = require('./scorer');
+
+  filter = require('./filter');
+
+  matcher = require('./matcher');
+
+  prepQueryCache = null;
+
+  module.exports = {
+    filter: function(candidates, query, options) {
+      if (options == null) {
+        options = {};
+      }
+      if (!((query != null ? query.length : void 0) && (candidates != null ? candidates.length : void 0))) {
+        return [];
+      }
+      options = parseOptions(options);
+      return filter(candidates, query, options);
+    },
+    prepQuery: function(query, options) {
+      if (options == null) {
+        options = {};
+      }
+      options = parseOptions(options);
+      return scorer.prepQuery(query, options);
+    },
+    score: function(string, query, prepQuery, options) {
+      var allowErrors, isPath, optCharRegEx, pathSeparator, useExtensionBonus;
+      if (options == null) {
+        options = {};
+      }
+      if (!((string != null ? string.length : void 0) && (query != null ? query.length : void 0))) {
+        return 0;
+      }
+      options = parseOptions(options);
+      allowErrors = options.allowErrors, isPath = options.isPath, useExtensionBonus = options.useExtensionBonus, optCharRegEx = options.optCharRegEx, pathSeparator = options.pathSeparator;
+      if (prepQuery == null) {
+        prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query, options));
+      }
+      return scorer.score(string, query, prepQuery, allowErrors, isPath, useExtensionBonus, pathSeparator);
+    },
+    match: function(string, query, prepQuery, options) {
+      var allowErrors, baseMatches, isPath, matches, optCharRegEx, pathSeparator, string_lw, useExtensionBonus, _i, _ref, _results;
+      if (options == null) {
+        options = {};
+      }
+      if (!string) {
+        return [];
+      }
+      if (!query) {
+        return [];
+      }
+      if (string === query) {
+        return (function() {
+          _results = [];
+          for (var _i = 0, _ref = string.length; 0 <= _ref ? _i < _ref : _i > _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); }
+          return _results;
+        }).apply(this);
+      }
+      options = parseOptions(options);
+      allowErrors = options.allowErrors, isPath = options.isPath, useExtensionBonus = options.useExtensionBonus, optCharRegEx = options.optCharRegEx, pathSeparator = options.pathSeparator;
+      if (prepQuery == null) {
+        prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query, options));
+      }
+      if (!(allowErrors || scorer.isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
+        return [];
+      }
+      string_lw = string.toLowerCase();
+      matches = matcher.match(string, string_lw, prepQuery);
+      if (matches.length === 0) {
+        return matches;
+      }
+      if (string.indexOf(pathSeparator) > -1) {
+        baseMatches = matcher.basenameMatch(string, string_lw, prepQuery, pathSeparator);
+        matches = matcher.mergeMatches(matches, baseMatches);
+      }
+      return matches;
+    }
+  };
+
+  parseOptions = function(options) {
+    if (options.allowErrors == null) {
+      options.allowErrors = false;
+    }
+    if (options.isPath == null) {
+      options.isPath = true;
+    }
+    if (options.useExtensionBonus == null) {
+      options.useExtensionBonus = true;
+    }
+    if (options.pathSeparator == null) {
+      options.pathSeparator = scorer.pathSeparator;
+    }
+    if (options.optCharRegEx == null) {
+      options.optCharRegEx = null;
+    }
+    return options;
+  };
+
+}).call(this);
+
+},{"./filter":1,"./matcher":3,"./scorer":4}],3:[function(require,module,exports){
+(function() {
+  var scorer;
+
+  scorer = require('./scorer');
+
+  exports.basenameMatch = function(subject, subject_lw, prepQuery, pathSeparator) {
+    var basePos, depth, end;
+    end = subject.length - 1;
+    while (subject[end] === pathSeparator) {
+      end--;
+    }
+    basePos = subject.lastIndexOf(pathSeparator, end);
+    if (basePos === -1) {
+      return [];
+    }
+    depth = prepQuery.depth;
+    while (depth-- > 0) {
+      basePos = subject.lastIndexOf(pathSeparator, basePos - 1);
+      if (basePos === -1) {
+        return [];
+      }
+    }
+    basePos++;
+    end++;
+    return exports.match(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery, basePos);
+  };
+
+  exports.mergeMatches = function(a, b) {
+    var ai, bj, i, j, m, n, out;
+    m = a.length;
+    n = b.length;
+    if (n === 0) {
+      return a.slice();
+    }
+    if (m === 0) {
+      return b.slice();
+    }
+    i = -1;
+    j = 0;
+    bj = b[j];
+    out = [];
+    while (++i < m) {
+      ai = a[i];
+      while (bj <= ai && ++j < n) {
+        if (bj < ai) {
+          out.push(bj);
+        }
+        bj = b[j];
+      }
+      out.push(ai);
+    }
+    while (j < n) {
+      out.push(b[j++]);
+    }
+    return out;
+  };
+
+  exports.match = function(subject, subject_lw, prepQuery, offset) {
+    var DIAGONAL, LEFT, STOP, UP, acro_score, align, backtrack, csc_diag, csc_row, csc_score, i, j, m, matches, move, n, pos, query, query_lw, score, score_diag, score_row, score_up, si_lw, start, trace;
+    if (offset == null) {
+      offset = 0;
+    }
+    query = prepQuery.query;
+    query_lw = prepQuery.query_lw;
+    m = subject.length;
+    n = query.length;
+    acro_score = scorer.scoreAcronyms(subject, subject_lw, query, query_lw).score;
+    score_row = new Array(n);
+    csc_row = new Array(n);
+    STOP = 0;
+    UP = 1;
+    LEFT = 2;
+    DIAGONAL = 3;
+    trace = new Array(m * n);
+    pos = -1;
+    j = -1;
+    while (++j < n) {
+      score_row[j] = 0;
+      csc_row[j] = 0;
+    }
+    i = -1;
+    while (++i < m) {
+      score = 0;
+      score_up = 0;
+      csc_diag = 0;
+      si_lw = subject_lw[i];
+      j = -1;
+      while (++j < n) {
+        csc_score = 0;
+        align = 0;
+        score_diag = score_up;
+        if (query_lw[j] === si_lw) {
+          start = scorer.isWordStart(i, subject, subject_lw);
+          csc_score = csc_diag > 0 ? csc_diag : scorer.scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
+          align = score_diag + scorer.scoreCharacter(i, j, start, acro_score, csc_score);
+        }
+        score_up = score_row[j];
+        csc_diag = csc_row[j];
+        if (score > score_up) {
+          move = LEFT;
+        } else {
+          score = score_up;
+          move = UP;
+        }
+        if (align > score) {
+          score = align;
+          move = DIAGONAL;
+        } else {
+          csc_score = 0;
+        }
+        score_row[j] = score;
+        csc_row[j] = csc_score;
+        trace[++pos] = score > 0 ? move : STOP;
+      }
+    }
+    i = m - 1;
+    j = n - 1;
+    pos = i * n + j;
+    backtrack = true;
+    matches = [];
+    while (backtrack && i >= 0 && j >= 0) {
+      switch (trace[pos]) {
+        case UP:
+          i--;
+          pos -= n;
+          break;
+        case LEFT:
+          j--;
+          pos--;
+          break;
+        case DIAGONAL:
+          matches.push(i + offset);
+          j--;
+          i--;
+          pos -= n + 1;
+          break;
+        default:
+          backtrack = false;
+      }
+    }
+    matches.reverse();
+    return matches;
+  };
+
+}).call(this);
+
+},{"./scorer":4}],4:[function(require,module,exports){
+(function (process){
+(function() {
+  var AcronymResult, Query, coreChars, countDir, defaultPathSeparator, emptyAcronymResult, file_coeff, getCharCodes, getExtension, getExtensionScore, isAcronymFullWord, isMatch, isSeparator, isWordEnd, isWordStart, miss_coeff, opt_char_re, pos_bonus, scoreAcronyms, scoreCharacter, scoreConsecutives, scoreExact, scoreExactMatch, scoreMain, scorePath, scorePattern, scorePosition, scoreSize, tau_depth, tau_size, truncatedUpperCase, wm;
+
+  defaultPathSeparator = exports.pathSeparator = process && (process.platform === "win32") ? '\\' : '/';
+
+  wm = 150;
+
+  pos_bonus = 20;
+
+  tau_depth = 13;
+
+  tau_size = 85;
+
+  file_coeff = 1.2;
+
+  miss_coeff = 0.75;
+
+  opt_char_re = /[ _\-:\/\\]/g;
+
+  exports.coreChars = coreChars = function(query, optCharRegEx) {
+    if (optCharRegEx == null) {
+      optCharRegEx = opt_char_re;
+    }
+    return query.replace(optCharRegEx, '');
+  };
+
+  exports.score = function(string, query, prepQuery, allowErrors, isPath, useExtensionBonus, pathSeparator) {
+    var score, string_lw;
+    if (!(allowErrors || isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
+      return 0;
+    }
+    string_lw = string.toLowerCase();
+    score = scoreMain(string, string_lw, prepQuery);
+    if (isPath) {
+      score = scorePath(string, string_lw, prepQuery, score, useExtensionBonus, pathSeparator);
+    }
+    return Math.ceil(score);
+  };
+
+  Query = (function() {
+    function Query(query, _arg) {
+      var optCharRegEx, pathSeparator, _ref;
+      _ref = _arg != null ? _arg : {}, optCharRegEx = _ref.optCharRegEx, pathSeparator = _ref.pathSeparator;
+      if (!(query && query.length)) {
+        return null;
+      }
+      this.query = query;
+      this.query_lw = query.toLowerCase();
+      this.core = coreChars(query, optCharRegEx);
+      this.core_lw = this.core.toLowerCase();
+      this.core_up = truncatedUpperCase(this.core);
+      this.depth = countDir(query, query.length, pathSeparator);
+      this.ext = getExtension(this.query_lw);
+      this.charCodes = getCharCodes(this.query_lw);
+    }
+
+    return Query;
+
+  })();
+
+  exports.prepQuery = function(query, options) {
+    return new Query(query, options);
+  };
+
+  exports.isMatch = isMatch = function(subject, query_lw, query_up) {
+    var i, j, m, n, qj_lw, qj_up, si;
+    m = subject.length;
+    n = query_lw.length;
+    if (!m || n > m) {
+      return false;
+    }
+    i = -1;
+    j = -1;
+    while (++j < n) {
+      qj_lw = query_lw.charCodeAt(j);
+      qj_up = query_up.charCodeAt(j);
+      while (++i < m) {
+        si = subject.charCodeAt(i);
+        if (si === qj_lw || si === qj_up) {
+          break;
+        }
+      }
+      if (i === m) {
+        return false;
+      }
+    }
+    return true;
+  };
+
+  scoreMain = function(subject, subject_lw, prepQuery) {
+    var acro, acro_score, align, csc_diag, csc_invalid, csc_row, csc_score, i, j, m, miss_budget, miss_left, mm, n, pos, query, query_lw, record_miss, score, score_diag, score_row, score_up, si_lw, start, sz;
+    query = prepQuery.query;
+    query_lw = prepQuery.query_lw;
+    m = subject.length;
+    n = query.length;
+    acro = scoreAcronyms(subject, subject_lw, query, query_lw);
+    acro_score = acro.score;
+    if (acro.count === n) {
+      return scoreExact(n, m, acro_score, acro.pos);
+    }
+    pos = subject_lw.indexOf(query_lw);
+    if (pos > -1) {
+      return scoreExactMatch(subject, subject_lw, query, query_lw, pos, n, m);
+    }
+    score_row = new Array(n);
+    csc_row = new Array(n);
+    sz = scoreSize(n, m);
+    miss_budget = Math.ceil(miss_coeff * n) + 5;
+    miss_left = miss_budget;
+    j = -1;
+    while (++j < n) {
+      score_row[j] = 0;
+      csc_row[j] = 0;
+    }
+    i = subject_lw.indexOf(query_lw[0]);
+    if (i > -1) {
+      i--;
+    }
+    mm = subject_lw.lastIndexOf(query_lw[n - 1], m);
+    if (mm > i) {
+      m = mm + 1;
+    }
+    csc_invalid = true;
+    while (++i < m) {
+      si_lw = subject_lw[i];
+      if (prepQuery.charCodes[si_lw.charCodeAt(0)] == null) {
+        if (csc_invalid !== true) {
+          j = -1;
+          while (++j < n) {
+            csc_row[j] = 0;
+          }
+          csc_invalid = true;
+        }
+        continue;
+      }
+      score = 0;
+      score_diag = 0;
+      csc_diag = 0;
+      record_miss = true;
+      csc_invalid = false;
+      j = -1;
+      while (++j < n) {
+        score_up = score_row[j];
+        if (score_up > score) {
+          score = score_up;
+        }
+        csc_score = 0;
+        if (query_lw[j] === si_lw) {
+          start = isWordStart(i, subject, subject_lw);
+          csc_score = csc_diag > 0 ? csc_diag : scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
+          align = score_diag + scoreCharacter(i, j, start, acro_score, csc_score);
+          if (align > score) {
+            score = align;
+            miss_left = miss_budget;
+          } else {
+            if (record_miss && --miss_left <= 0) {
+              return score_row[n - 1] * sz;
+            }
+            record_miss = false;
+          }
+        }
+        score_diag = score_up;
+        csc_diag = csc_row[j];
+        csc_row[j] = csc_score;
+        score_row[j] = score;
+      }
+    }
+    score = score_row[n - 1];
+    return score * sz;
+  };
+
+  exports.isWordStart = isWordStart = function(pos, subject, subject_lw) {
+    var curr_s, prev_s;
+    if (pos === 0) {
+      return true;
+    }
+    curr_s = subject[pos];
+    prev_s = subject[pos - 1];
+    return isSeparator(prev_s) || (curr_s !== subject_lw[pos] && prev_s === subject_lw[pos - 1]);
+  };
+
+  exports.isWordEnd = isWordEnd = function(pos, subject, subject_lw, len) {
+    var curr_s, next_s;
+    if (pos === len - 1) {
+      return true;
+    }
+    curr_s = subject[pos];
+    next_s = subject[pos + 1];
+    return isSeparator(next_s) || (curr_s === subject_lw[pos] && next_s !== subject_lw[pos + 1]);
+  };
+
+  isSeparator = function(c) {
+    return c === ' ' || c === '.' || c === '-' || c === '_' || c === '/' || c === '\\';
+  };
+
+  scorePosition = function(pos) {
+    var sc;
+    if (pos < pos_bonus) {
+      sc = pos_bonus - pos;
+      return 100 + sc * sc;
+    } else {
+      return Math.max(100 + pos_bonus - pos, 0);
+    }
+  };
+
+  scoreSize = function(n, m) {
+    return tau_size / (tau_size + Math.abs(m - n));
+  };
+
+  scoreExact = function(n, m, quality, pos) {
+    return 2 * n * (wm * quality + scorePosition(pos)) * scoreSize(n, m);
+  };
+
+  exports.scorePattern = scorePattern = function(count, len, sameCase, start, end) {
+    var bonus, sz;
+    sz = count;
+    bonus = 6;
+    if (sameCase === count) {
+      bonus += 2;
+    }
+    if (start) {
+      bonus += 3;
+    }
+    if (end) {
+      bonus += 1;
+    }
+    if (count === len) {
+      if (start) {
+        if (sameCase === len) {
+          sz += 2;
+        } else {
+          sz += 1;
+        }
+      }
+      if (end) {
+        bonus += 1;
+      }
+    }
+    return sameCase + sz * (sz + bonus);
+  };
+
+  exports.scoreCharacter = scoreCharacter = function(i, j, start, acro_score, csc_score) {
+    var posBonus;
+    posBonus = scorePosition(i);
+    if (start) {
+      return posBonus + wm * ((acro_score > csc_score ? acro_score : csc_score) + 10);
+    }
+    return posBonus + wm * csc_score;
+  };
+
+  exports.scoreConsecutives = scoreConsecutives = function(subject, subject_lw, query, query_lw, i, j, startOfWord) {
+    var k, m, mi, n, nj, sameCase, sz;
+    m = subject.length;
+    n = query.length;
+    mi = m - i;
+    nj = n - j;
+    k = mi < nj ? mi : nj;
+    sameCase = 0;
+    sz = 0;
+    if (query[j] === subject[i]) {
+      sameCase++;
+    }
+    while (++sz < k && query_lw[++j] === subject_lw[++i]) {
+      if (query[j] === subject[i]) {
+        sameCase++;
+      }
+    }
+    if (sz === 1) {
+      return 1 + 2 * sameCase;
+    }
+    return scorePattern(sz, n, sameCase, startOfWord, isWordEnd(i, subject, subject_lw, m));
+  };
+
+  exports.scoreExactMatch = scoreExactMatch = function(subject, subject_lw, query, query_lw, pos, n, m) {
+    var end, i, pos2, sameCase, start;
+    start = isWordStart(pos, subject, subject_lw);
+    if (!start) {
+      pos2 = subject_lw.indexOf(query_lw, pos + 1);
+      if (pos2 > -1) {
+        start = isWordStart(pos2, subject, subject_lw);
+        if (start) {
+          pos = pos2;
+        }
+      }
+    }
+    i = -1;
+    sameCase = 0;
+    while (++i < n) {
+      if (query[pos + i] === subject[i]) {
+        sameCase++;
+      }
+    }
+    end = isWordEnd(pos + n - 1, subject, subject_lw, m);
+    return scoreExact(n, m, scorePattern(n, n, sameCase, start, end), pos);
+  };
+
+  AcronymResult = (function() {
+    function AcronymResult(score, pos, count) {
+      this.score = score;
+      this.pos = pos;
+      this.count = count;
+    }
+
+    return AcronymResult;
+
+  })();
+
+  emptyAcronymResult = new AcronymResult(0, 0.1, 0);
+
+  exports.scoreAcronyms = scoreAcronyms = function(subject, subject_lw, query, query_lw) {
+    var count, fullWord, i, j, m, n, qj_lw, sameCase, score, sepCount, sumPos;
+    m = subject.length;
+    n = query.length;
+    if (!(m > 1 && n > 1)) {
+      return emptyAcronymResult;
+    }
+    count = 0;
+    sepCount = 0;
+    sumPos = 0;
+    sameCase = 0;
+    i = -1;
+    j = -1;
+    while (++j < n) {
+      qj_lw = query_lw[j];
+      if (isSeparator(qj_lw)) {
+        i = subject_lw.indexOf(qj_lw, i + 1);
+        if (i > -1) {
+          sepCount++;
+          continue;
+        } else {
+          break;
+        }
+      }
+      while (++i < m) {
+        if (qj_lw === subject_lw[i] && isWordStart(i, subject, subject_lw)) {
+          if (query[j] === subject[i]) {
+            sameCase++;
+          }
+          sumPos += i;
+          count++;
+          break;
+        }
+      }
+      if (i === m) {
+        break;
+      }
+    }
+    if (count < 2) {
+      return emptyAcronymResult;
+    }
+    fullWord = count === n ? isAcronymFullWord(subject, subject_lw, query, count) : false;
+    score = scorePattern(count, n, sameCase, true, fullWord);
+    return new AcronymResult(score, sumPos / count, count + sepCount);
+  };
+
+  isAcronymFullWord = function(subject, subject_lw, query, nbAcronymInQuery) {
+    var count, i, m, n;
+    m = subject.length;
+    n = query.length;
+    count = 0;
+    if (m > 12 * n) {
+      return false;
+    }
+    i = -1;
+    while (++i < m) {
+      if (isWordStart(i, subject, subject_lw) && ++count > nbAcronymInQuery) {
+        return false;
+      }
+    }
+    return true;
+  };
+
+  scorePath = function(subject, subject_lw, prepQuery, fullPathScore, useExtensionBonus, pathSeparator) {
+    var alpha, basePathScore, basePos, depth, end, extAdjust;
+    if (fullPathScore === 0) {
+      return 0;
+    }
+    end = subject.length - 1;
+    while (subject[end] === pathSeparator) {
+      end--;
+    }
+    basePos = subject.lastIndexOf(pathSeparator, end);
+    extAdjust = 1.0;
+    if (useExtensionBonus) {
+      extAdjust += getExtensionScore(subject_lw, prepQuery.ext, basePos, end, 2);
+      fullPathScore *= extAdjust;
+    }
+    if (basePos === -1) {
+      return fullPathScore;
+    }
+    depth = prepQuery.depth;
+    while (basePos > -1 && depth-- > 0) {
+      basePos = subject.lastIndexOf(pathSeparator, basePos - 1);
+    }
+    basePathScore = basePos === -1 ? fullPathScore : extAdjust * scoreMain(subject.slice(basePos + 1, end + 1), subject_lw.slice(basePos + 1, end + 1), prepQuery);
+    alpha = 0.5 * tau_depth / (tau_depth + countDir(subject, end + 1, pathSeparator));
+    return alpha * basePathScore + (1 - alpha) * fullPathScore * scoreSize(0, file_coeff * (end - basePos));
+  };
+
+  exports.countDir = countDir = function(path, end, pathSeparator) {
+    var count, i;
+    if (end < 1) {
+      return 0;
+    }
+    count = 0;
+    i = -1;
+    while (++i < end && path[i] === pathSeparator) {
+      continue;
+    }
+    while (++i < end) {
+      if (path[i] === pathSeparator) {
+        count++;
+        while (++i < end && path[i] === pathSeparator) {
+          continue;
+        }
+      }
+    }
+    return count;
+  };
+
+  getExtension = function(str) {
+    var pos;
+    pos = str.lastIndexOf(".");
+    if (pos < 0) {
+      return "";
+    } else {
+      return str.substr(pos + 1);
+    }
+  };
+
+  getExtensionScore = function(candidate, ext, startPos, endPos, maxDepth) {
+    var m, matched, n, pos;
+    if (!ext.length) {
+      return 0;
+    }
+    pos = candidate.lastIndexOf(".", endPos);
+    if (!(pos > startPos)) {
+      return 0;
+    }
+    n = ext.length;
+    m = endPos - pos;
+    if (m < n) {
+      n = m;
+      m = ext.length;
+    }
+    pos++;
+    matched = -1;
+    while (++matched < n) {
+      if (candidate[pos + matched] !== ext[matched]) {
+        break;
+      }
+    }
+    if (matched === 0 && maxDepth > 0) {
+      return 0.9 * getExtensionScore(candidate, ext, startPos, pos - 2, maxDepth - 1);
+    }
+    return matched / m;
+  };
+
+  truncatedUpperCase = function(str) {
+    var char, upper, _i, _len;
+    upper = "";
+    for (_i = 0, _len = str.length; _i < _len; _i++) {
+      char = str[_i];
+      upper += char.toUpperCase()[0];
+    }
+    return upper;
+  };
+
+  getCharCodes = function(str) {
+    var charCodes, i, len;
+    len = str.length;
+    i = -1;
+    charCodes = [];
+    while (++i < len) {
+      charCodes[str.charCodeAt(i)] = true;
+    }
+    return charCodes;
+  };
+
+}).call(this);
+
+}).call(this,require('_process'))
+},{"_process":5}],5:[function(require,module,exports){
+// shim for using process in browser
+
+var process = module.exports = {};
+
+// cached from whatever global is present so that test runners that stub it
+// don't break things.  But we need to wrap it in a try catch in case it is
+// wrapped in strict mode code which doesn't define any globals.  It's inside a
+// function because try/catches deoptimize in certain engines.
+
+var cachedSetTimeout;
+var cachedClearTimeout;
+
+(function () {
+  try {
+    cachedSetTimeout = setTimeout;
+  } catch (e) {
+    cachedSetTimeout = function () {
+      throw new Error('setTimeout is not defined');
+    }
+  }
+  try {
+    cachedClearTimeout = clearTimeout;
+  } catch (e) {
+    cachedClearTimeout = function () {
+      throw new Error('clearTimeout is not defined');
+    }
+  }
+} ())
+var queue = [];
+var draining = false;
+var currentQueue;
+var queueIndex = -1;
+
+function cleanUpNextTick() {
+    if (!draining || !currentQueue) {
+        return;
+    }
+    draining = false;
+    if (currentQueue.length) {
+        queue = currentQueue.concat(queue);
+    } else {
+        queueIndex = -1;
+    }
+    if (queue.length) {
+        drainQueue();
+    }
+}
+
+function drainQueue() {
+    if (draining) {
+        return;
+    }
+    var timeout = cachedSetTimeout(cleanUpNextTick);
+    draining = true;
+
+    var len = queue.length;
+    while(len) {
+        currentQueue = queue;
+        queue = [];
+        while (++queueIndex < len) {
+            if (currentQueue) {
+                currentQueue[queueIndex].run();
+            }
+        }
+        queueIndex = -1;
+        len = queue.length;
+    }
+    currentQueue = null;
+    draining = false;
+    cachedClearTimeout(timeout);
+}
+
+process.nextTick = function (fun) {
+    var args = new Array(arguments.length - 1);
+    if (arguments.length > 1) {
+        for (var i = 1; i < arguments.length; i++) {
+            args[i - 1] = arguments[i];
+        }
+    }
+    queue.push(new Item(fun, args));
+    if (queue.length === 1 && !draining) {
+        cachedSetTimeout(drainQueue, 0);
+    }
+};
+
+// v8 likes predictible objects
+function Item(fun, array) {
+    this.fun = fun;
+    this.array = array;
+}
+Item.prototype.run = function () {
+    this.fun.apply(null, this.array);
+};
+process.title = 'browser';
+process.browser = true;
+process.env = {};
+process.argv = [];
+process.version = ''; // empty string to avoid regexp issues
+process.versions = {};
+
+function noop() {}
+
+process.on = noop;
+process.addListener = noop;
+process.once = noop;
+process.off = noop;
+process.removeListener = noop;
+process.removeAllListeners = noop;
+process.emit = noop;
+
+process.binding = function (name) {
+    throw new Error('process.binding is not supported');
+};
+
+process.cwd = function () { return '/' };
+process.chdir = function (dir) {
+    throw new Error('process.chdir is not supported');
+};
+process.umask = function() { return 0; };
+
+},{}]},{},[2])(2)
+});
\ No newline at end of file
diff --git a/dist-browser/fuzzaldrin-plus.min.js b/dist-browser/fuzzaldrin-plus.min.js
new file mode 100644
index 0000000..38531d9
--- /dev/null
+++ b/dist-browser/fuzzaldrin-plus.min.js
@@ -0,0 +1,2 @@
+/* fuzzaldrin-plus - v0.3.1 - @license: MIT; @author: Jean Christophe Roy; @site: https://github.com/jeancroy/fuzzaldrin-plus */
+!function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.fuzzaldrin=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw  [...]
\ No newline at end of file
diff --git a/package.json b/package.json
index baeaea2..30166f7 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,8 @@
 {
   "name": "fuzzaldrin-plus",
   "version": "0.3.1",
-  "description": "Fuzzy filtering and string scoring - compatible with fuzzaldrin",
+  "description": "Fuzzy filtering and string similarity scoring - compatible with fuzzaldrin",
+  "license": "MIT",
   "licenses": [
     {
       "type": "MIT",
@@ -33,12 +34,15 @@
     "sublime"
   ],
   "devDependencies": {
-    "jasmine-focused": "1.x",
-    "grunt-contrib-coffee": "~0.9.0",
-    "grunt-cli": "~0.1.8",
+    "coffee-script": "~1.7",
+    "fuzzaldrin": "~2.1.0",
     "grunt": "~0.4.1",
-    "grunt-shell": "~0.2.2",
+    "grunt-browserify": "^5.0.0",
+    "grunt-cli": "~0.1.8",
     "grunt-coffeelint": "0.0.6",
-    "coffee-script": "~1.7"
+    "grunt-contrib-coffee": "~0.9.0",
+    "grunt-contrib-uglify": "^1.0.1",
+    "grunt-shell": "~0.2.2",
+    "jasmine-focused": "1.x"
   }
 }
diff --git a/spec/filter-spec.coffee b/spec/filter-spec.coffee
index 8725dbb..7778fb2 100644
--- a/spec/filter-spec.coffee
+++ b/spec/filter-spec.coffee
@@ -1,13 +1,14 @@
 path = require 'path'
 {filter,score} = require '../src/fuzzaldrin'
 
-bestMatch = (candidates, query, {debug}={}) ->
+bestMatch = (candidates, query, options = {}) ->
 
+  {debug} = options
   if debug?
     console.log("\n = Against query: #{query} = ")
     console.log(" #{score(c, query)}: #{c}") for c in candidates
 
-  filter(candidates, query, maxResults: 1)[0]
+  filter(candidates, query, options)[0]
 
 rootPath = (segments...) ->
   joinedPath = if process.platform is 'win32' then 'C:\\' else '/'
@@ -31,10 +32,10 @@ describe "filtering", ->
 
   it "support unicode character with different length uppercase", ->
 
-    candidates = ["Bernauer Stra\u00DFe Wall"] # Bernauer Stra�e Wall
+    candidates = ["Bernauer Stra\u00DFe Wall"] # Bernauer Straße Wall
     expect(filter(candidates, 'Stra\u00DFe Wall')).toEqual candidates
-  # before correction, The map �->SS , place the W out of sync and prevent a match.
-  # After correction we map �->S.
+  # before correction, The map ß->SS , place the W out of sync and prevent a match.
+  # After correction we map ß->S.
 
   describe "when the maxResults option is set", ->
     it "limits the results to the result size", ->
@@ -135,7 +136,7 @@ describe "filtering", ->
 
       candidates = [
         'TODO',
-        path.join('doc','README')
+        path.join('doc', 'README')
       ]
       expect(bestMatch(candidates, 'd')).toBe candidates[1]
 
@@ -148,7 +149,6 @@ describe "filtering", ->
       expect(bestMatch(candidates, 'es')).toBe candidates[0]
 
 
-
   #---------------------------------------------------
   #
   #                  Consecutive letters
@@ -297,7 +297,7 @@ describe "filtering", ->
       expect(bestMatch(['sub_zero', 'sub-zero', 'sub zero'], 'sz')).toBe 'sub_zero'
 
 
-    it "weighs CamelCase matches higher", ->
+    it "weighs acronym matches higher than middle of word letter", ->
 
       candidates = [
         'FilterFactors.html',
@@ -430,8 +430,32 @@ describe "filtering", ->
       expect(bestMatch(candidates, 'CCCa')).toBe candidates[0]
       expect(bestMatch(candidates, 'ccca')).toBe candidates[1]
 
+    it "prefers acronym matches that correspond to the full candidate acronym", ->
+      candidates = [
+        'JaVaScript',
+        'JavaScript'
+      ]
 
+      # <js vs JS> scores better than <js vs JVS>
+      expect(bestMatch(candidates, 'js')).toBe candidates[1]
 
+      candidates = [
+        'JSON',
+        'J.S.O.N.',
+        'JavaScript'
+      ]
+
+      # here 1:1 match outdo shorter start-of-word
+      expect(bestMatch(candidates, 'js')).toBe candidates[2]
+
+      candidates = [
+        'CSON',
+        'C.S.O.N.',
+        'CoffeeScript'
+      ]
+
+      # here 1:1 match outdo shorter start-of-word
+      expect(bestMatch(candidates, 'cs')).toBe candidates[2]
 
 
   #---------------------------------------------------
@@ -476,13 +500,24 @@ describe "filtering", ->
     it "prefers matches that are together in the basename (even if basename is longer)", ->
 
       candidates = [
-        path.join('tests','buyers','orders_e2e.js'),
-        path.join('tests','buyers','users-addresses_e2e.js')
+        path.join('tests', 'buyers', 'orders_e2e.js'),
+        path.join('tests', 'buyers', 'users-addresses_e2e.js')
       ]
 
       expect(bestMatch(candidates, 'us_e2')).toBe candidates[1]
 
 
+      candidates = [
+        path.join('app', 'controllers', 'match_controller.rb'),
+        path.join('app', 'controllers', 'application_controller.rb')
+      ]
+
+      expect(bestMatch(candidates, 'appcontr')).toBe candidates[1]
+      expect(bestMatch(candidates, 'appcontro')).toBe candidates[1]
+    #expect(bestMatch(candidates, 'appcontrol', debug:true)).toBe candidates[1] # TODO support this case ?
+    #expect(bestMatch(candidates, 'appcontroll', debug:true)).toBe candidates[1] #Also look at issue #6
+
+
     it "allows to select using folder name", ->
 
       candidates = [
@@ -529,6 +564,31 @@ describe "filtering", ->
       expect(bestMatch(candidates, 'core')).toBe candidates[1]
       expect(bestMatch(candidates, 'foo')).toBe candidates[0]
 
+    it "prefers file of the specified extension when useExtensionBonus is true ", ->
+
+      candidates = [
+        path.join('meas_astrom', 'include', 'Isst', 'meas', 'astrom', 'matchOptimisticB.h')
+        path.join('IsstDoxygen', 'html', 'match_optimistic_b_8cc.html')
+      ]
+
+      expect(bestMatch(candidates, 'mob.h', {useExtensionBonus: true})).toBe candidates[0]
+
+      candidates = [
+        path.join('matchOptimisticB.htaccess')
+        path.join('matchOptimisticB_main.html')
+      ]
+
+      expect(bestMatch(candidates, 'mob.ht', {useExtensionBonus: true})).toBe candidates[1]
+
+    it "support file with multiple extension", ->
+      candidates = [
+        path.join('something-foobar.class')
+        path.join('something.class.php')
+      ]
+
+      expect(bestMatch(candidates, 'some.cl', {useExtensionBonus: true})).toBe candidates[1]
+
+
     it "ignores trailing slashes", ->
 
       candidates = [
@@ -539,7 +599,7 @@ describe "filtering", ->
       expect(bestMatch(candidates, 'br')).toBe candidates[1]
 
 
-    it "allows candidate to be all slashes", ->
+    it "allows candidates to be all slashes", ->
       candidates = [path.sep, path.sep + path.sep + path.sep]
       expect(filter(candidates, 'bar')).toEqual []
 
@@ -556,36 +616,59 @@ describe "filtering", ->
       expect(bestMatch(candidates, 'project file')).toBe candidates[0]
       expect(bestMatch(candidates, path.join('project', 'file'))).toBe candidates[1]
 
+    it "prefers overall better match to shorter end-of-path length", ->
+
+      candidates = [
+
+        path.join('CommonControl', 'Controls', 'Shared')
+        path.join('CommonControl', 'Controls', 'Shared', 'Mouse')
+        path.join('CommonControl', 'Controls', 'Shared', 'Keyboard')
+        path.join('CommonControl', 'Controls', 'Shared', 'Keyboard', 'cc.js')
+
+      ]
+
+      expect(bestMatch(candidates, path.join('CC','Controls','Shared'))).toBe candidates[0]
+      expect(bestMatch(candidates, 'CC Controls Shared')).toBe candidates[0]
+
+
+      expect(bestMatch(candidates, 'CCCShared')).toBe candidates[0]
+      expect(bestMatch(candidates, 'ccc shared')).toBe candidates[0]
+      expect(bestMatch(candidates, 'cc c shared')).toBe candidates[0]
+
+      expect(bestMatch(candidates, path.join('ccc','shared'))).toBe candidates[0]
+      expect(bestMatch(candidates, path.join('cc','c','shared'))).toBe candidates[0]
+
+
 
   describe "when the entries are of differing directory depths", ->
 
     it "prefers shallow path", ->
 
-      candidate = [
+      candidates = [
         path.join('b', 'z', 'file'),
         path.join('b_z', 'file')
       ]
 
-      expect(bestMatch(candidate, "file")).toBe candidate[1]
-      expect(bestMatch(candidate, "fle")).toBe candidate[1]
+      expect(bestMatch(candidates, "file")).toBe candidates[1]
+      expect(bestMatch(candidates, "fle")).toBe candidates[1]
 
-      candidate = [
+      candidates = [
         path.join('foo', 'bar', 'baz', 'file'),
         path.join('foo', 'bar_baz', 'file')
       ]
 
-      expect(bestMatch(candidate, "file")).toBe candidate[1]
-      expect(bestMatch(candidate, "fle")).toBe candidate[1]
+      expect(bestMatch(candidates, "file")).toBe candidates[1]
+      expect(bestMatch(candidates, "fle")).toBe candidates[1]
 
-      candidate = [
+      candidates = [
         path.join('A Long User Full-Name', 'My Documents', 'file'),
         path.join('bin', 'lib', 'src', 'test', 'spec', 'file')
       ]
 
-      expect(bestMatch(candidate, "file")).toBe candidate[0]
+      expect(bestMatch(candidates, "file")).toBe candidates[0]
 
-      # We have plenty of report on how this or that should win because file is a better basename match
-      # But we have no report of searching too deep, because of that folder-depth penalty is pretty weak.
+    # We have plenty of report on how this or that should win because file is a better basename match
+    # But we have no report of searching too deep, because of that folder-depth penalty is pretty weak.
 
 
     it "allows better basename match to overcome slightly deeper directory / longer overall path", ->
@@ -689,19 +772,19 @@ describe "filtering", ->
 
     it "allows to match path using either backward slash, forward slash, space or colon", ->
 
-      candidate = [
+      candidates = [
         path.join('foo', 'bar'),
         path.join('model', 'user'),
       ]
 
-      expect(bestMatch(candidate, "model user")).toBe candidate[1]
-      expect(bestMatch(candidate, "model/user")).toBe candidate[1]
-      expect(bestMatch(candidate, "model\\user")).toBe candidate[1]
-      expect(bestMatch(candidate, "model::user")).toBe candidate[1]
+      expect(bestMatch(candidates, "model user")).toBe candidates[1]
+      expect(bestMatch(candidates, "model/user")).toBe candidates[1]
+      expect(bestMatch(candidates, "model\\user")).toBe candidates[1]
+      expect(bestMatch(candidates, "model::user")).toBe candidates[1]
 
     it "prefer matches where the optional character is present", ->
 
-      candidate = [
+      candidates = [
         'ModelUser',
         'model user',
         'model/user',
@@ -711,12 +794,12 @@ describe "filtering", ->
         'model-user',
       ]
 
-      expect(bestMatch(candidate, "mdl user")).toBe candidate[1]
-      expect(bestMatch(candidate, "mdl/user")).toBe candidate[2]
-      expect(bestMatch(candidate, "mdl\\user")).toBe candidate[3]
-      expect(bestMatch(candidate, "mdl::user")).toBe candidate[4]
-      expect(bestMatch(candidate, "mdl_user")).toBe candidate[5]
-      expect(bestMatch(candidate, "mdl-user")).toBe candidate[6]
+      expect(bestMatch(candidates, "mdl user")).toBe candidates[1]
+      expect(bestMatch(candidates, "mdl/user")).toBe candidates[2]
+      expect(bestMatch(candidates, "mdl\\user")).toBe candidates[3]
+      expect(bestMatch(candidates, "mdl::user")).toBe candidates[4]
+      expect(bestMatch(candidates, "mdl_user")).toBe candidates[5]
+      expect(bestMatch(candidates, "mdl-user")).toBe candidates[6]
 
 
     it "weighs basename matches higher (space don't have a strict preference for slash)", ->
@@ -732,43 +815,47 @@ describe "filtering", ->
       # Without support for optional character, the basename bonus
       # would not be able to find "model" inside "user.rb" so the bonus would be 0
 
-      candidate = [
+      candidates = [
         path.join('www', 'lib', 'models', 'user.rb'),
         path.join('migrate', 'moderator_column_users.rb')
       ]
 
-      expect(bestMatch(candidate, "model user")).toBe candidate[0]
-      expect(bestMatch(candidate, "modeluser")).toBe candidate[0]
-      expect(bestMatch(candidate, path.join("model", "user"))).toBe candidate[0]
+      expect(bestMatch(candidates, "model user")).toBe candidates[0]
+      expect(bestMatch(candidates, "modeluser")).toBe candidates[0]
+      expect(bestMatch(candidates, path.join("model", "user"))).toBe candidates[0]
 
-      candidate = [
+      candidates = [
         path.join('destroy_discard_pool.png'),
         path.join('resources', 'src', 'app_controller.coffee')
       ]
 
-      expect(bestMatch(candidate, "src app")).toBe candidate[1]
-      expect(bestMatch(candidate, path.join("src", "app"))).toBe candidate[1]
+      expect(bestMatch(candidates, "src app")).toBe candidates[1]
+      expect(bestMatch(candidates, path.join("src", "app"))).toBe candidates[1]
 
-      candidate = [
+      candidates = [
         path.join('template', 'emails-dialogs.handlebars'),
         path.join('emails', 'handlers.py')
       ]
 
-      expect(bestMatch(candidate, "email handlers")).toBe candidate[1]
-      expect(bestMatch(candidate, path.join("email", "handlers"))).toBe candidate[1]
+      expect(bestMatch(candidates, "email handlers")).toBe candidates[1]
+      expect(bestMatch(candidates, path.join("email", "handlers"))).toBe candidates[1]
 
 
     it "allows to select between full query and basename using path.sep", ->
 
-      candidate = [
+      candidates = [
         path.join('models', 'user.rb'),
         path.join('migrate', 'model_users.rb')
       ]
 
-      expect(bestMatch(candidate, "modeluser")).toBe candidate[1]
-      expect(bestMatch(candidate, "model user")).toBe candidate[1]
-      expect(bestMatch(candidate, path.join("model","user"))).toBe candidate[0]
+      expect(bestMatch(candidates, "modeluser")).toBe candidates[1]
+      expect(bestMatch(candidates, "model user")).toBe candidates[1]
+      expect(bestMatch(candidates, path.join("model", "user"))).toBe candidates[0]
 
+  describe "when query is made only of optional characters", ->
+    it "only return results having at least one specified optional character", ->
+      candidates = ["bla", "_test", " test"]
+      expect(filter(candidates, '_')).toEqual ['_test']
 
 
   #---------------------------------------------------
@@ -805,13 +892,13 @@ describe "filtering", ->
       expect(result[3]).toBe candidates[1]
       expect(result[4]).toBe candidates[0]
 
-      # for this one, complete word "Install" should win against:
-      #
-      #  - case-sensitive end-of-word match "Uninstall",
-      #  - start of word match "Installed",
-      #  - double acronym match "in S t A ll" -> "Select All"
-      #
-      #  also "Install" by itself should win against "Install" in a sentence
+    # for this one, complete word "Install" should win against:
+    #
+    #  - case-sensitive end-of-word match "Uninstall",
+    #  - start of word match "Installed",
+    #  - double acronym match "in S t A ll" -> "Select All"
+    #
+    #  also "Install" by itself should win against "Install" in a sentence
 
 
     it "weighs substring higher than individual characters", ->
@@ -826,6 +913,6 @@ describe "filtering", ->
       expect(bestMatch(candidates, 'git push')).toBe candidates[2]
       expect(bestMatch(candidates, 'gpush')).toBe candidates[2]
 
-      # Here "Plus Stage Hunk" accidentally match acronym on PuSH.
-      # Using two words disable exactMatch bonus, we have to rely on consecutive match
+# Here "Plus Stage Hunk" accidentally match acronym on PuSH.
+# Using two words disable exactMatch bonus, we have to rely on consecutive match
 
diff --git a/spec/legacy-filter-spec.coffee b/spec/legacy-filter-spec.coffee
deleted file mode 100644
index dfd5ccf..0000000
--- a/spec/legacy-filter-spec.coffee
+++ /dev/null
@@ -1,142 +0,0 @@
-path = require 'path'
-fuzzaldrin = require '../src/fuzzaldrin'
-
-filter = (candidates, query) ->
-  fuzzaldrin.filter(candidates, query, {legacy:true})
-
-bestMatch = (candidates, query) ->
-  fuzzaldrin.filter(candidates, query, { maxResults: 1, legacy:true})[0]
-
-rootPath = (segments...) ->
-  joinedPath = if process.platform is 'win32' then 'C:\\' else '/'
-  for segment in segments
-    if segment is path.sep
-      joinedPath += segment
-    else
-      joinedPath = path.join(joinedPath, segment)
-  joinedPath
-
-describe "filtering", ->
-  it "returns an array of the most accurate results", ->
-    candidates = ['Gruntfile','filter', 'bile', null, '', undefined]
-    expect(filter(candidates, 'file')).toEqual ['filter', 'Gruntfile']
-
-  describe "when the maxResults option is set", ->
-    it "limits the results to the result size", ->
-      candidates = ['Gruntfile', 'filter', 'bile']
-      expect(bestMatch(candidates, 'file')).toBe 'filter'
-
-  describe "when the entries contains slashes", ->
-    it "weighs basename matches higher", ->
-      candidates = [
-        rootPath('bar', 'foo')
-        rootPath('foo', 'bar')
-      ]
-      expect(bestMatch(candidates, 'bar')).toBe candidates[1]
-
-      candidates = [
-        rootPath('bar', 'foo')
-        rootPath('foo', 'bar', path.sep, path.sep, path.sep, path.sep, path.sep)
-      ]
-      expect(bestMatch(candidates, 'bar')).toBe candidates[1]
-
-      candidates = [
-        rootPath('bar', 'foo')
-        rootPath('foo', 'bar')
-        'bar'
-      ]
-      expect(bestMatch(candidates, 'bar')).toEqual candidates[2]
-
-      candidates = [
-        rootPath('bar', 'foo')
-        rootPath('foo', 'bar')
-        rootPath('bar')
-      ]
-      expect(bestMatch(candidates, 'bar')).toBe candidates[2]
-
-      candidates = [
-        rootPath('bar', 'foo')
-        "bar#{path.sep}#{path.sep}#{path.sep}#{path.sep}#{path.sep}#{path.sep}"
-      ]
-      expect(bestMatch(candidates, 'bar')).toBe candidates[1]
-
-      expect(bestMatch([path.join('f', 'o', '1_a_z'), path.join('f', 'o', 'a_z')], 'az')).toBe path.join('f', 'o', 'a_z')
-      expect(bestMatch([path.join('f', '1_a_z'), path.join('f', 'o', 'a_z')], 'az')).toBe path.join('f', 'o', 'a_z')
-
-  describe "when the candidate is all slashes", ->
-    it "does not throw an exception", ->
-      candidates = [path.sep]
-      expect(filter(candidates, 'bar', maxResults: 1)).toEqual []
-
-  describe "when the entries contains spaces", ->
-    it "treats spaces as slashes", ->
-      candidates = [
-        rootPath('bar', 'foo')
-        rootPath('foo', 'bar')
-      ]
-      expect(bestMatch(candidates, 'br f')).toBe candidates[0]
-
-    it "weighs basename matches higher", ->
-      candidates = [
-        rootPath('bar', 'foo')
-        rootPath('foo', 'bar foo')
-      ]
-      expect(bestMatch(candidates, 'br f')).toBe candidates[1]
-
-      candidates = [
-        rootPath('barfoo', 'foo')
-        rootPath('foo', 'barfoo')
-      ]
-      expect(bestMatch(candidates, 'br f')).toBe candidates[1]
-
-      candidates = [
-        path.join('lib', 'exportable.rb')
-        path.join('app', 'models', 'table.rb')
-      ]
-      expect(bestMatch(candidates, 'table')).toBe candidates[1]
-
-  describe "when the entries contains mixed case", ->
-    it "weighs exact case matches higher", ->
-      candidates = ['statusurl', 'StatusUrl']
-      expect(bestMatch(candidates, 'Status')).toBe 'StatusUrl'
-      expect(bestMatch(candidates, 'SU')).toBe 'StatusUrl'
-      expect(bestMatch(candidates, 'status')).toBe 'statusurl'
-      expect(bestMatch(candidates, 'su')).toBe 'statusurl'
-      expect(bestMatch(candidates, 'statusurl')).toBe 'statusurl'
-      expect(bestMatch(candidates, 'StatusUrl')).toBe 'StatusUrl'
-
-  it "weighs abbreviation matches after spaces, underscores, and dashes the same", ->
-    expect(bestMatch(['sub-zero', 'sub zero', 'sub_zero'], 'sz')).toBe 'sub-zero'
-    expect(bestMatch(['sub zero', 'sub_zero', 'sub-zero'], 'sz')).toBe 'sub zero'
-    expect(bestMatch(['sub_zero', 'sub-zero', 'sub zero'], 'sz')).toBe 'sub_zero'
-
-  it "weighs matches at the start of the string or base name higher", ->
-    expect(bestMatch(['a_b_c', 'a_b'], 'ab')).toBe 'a_b'
-    expect(bestMatch(['z_a_b', 'a_b'], 'ab')).toBe 'a_b'
-    expect(bestMatch(['a_b_c', 'c_a_b'], 'ab')).toBe 'a_b_c'
-
-  describe "when the entries are of differing directory depths", ->
-    it "places exact matches first, even if they're deeper", ->
-      candidates = [
-        path.join('app', 'models', 'automotive', 'car.rb')
-        path.join('spec', 'factories', 'cars.rb')
-      ]
-      expect(bestMatch(candidates, 'car.rb')).toBe candidates[0]
-
-      candidates = [
-        path.join('app', 'models', 'automotive', 'car.rb')
-        'car.rb'
-      ]
-      expect(bestMatch(candidates, 'car.rb')).toBe candidates[1]
-
-      candidates = [
-        'car.rb',
-        path.join('app', 'models', 'automotive', 'car.rb')
-      ]
-      expect(bestMatch(candidates, 'car.rb')).toBe candidates[0]
-
-      candidates = [
-        path.join('app', 'models', 'cars', 'car.rb')
-        path.join('spec', 'cars.rb')
-      ]
-      expect(bestMatch(candidates, 'car.rb')).toBe candidates[0]
\ No newline at end of file
diff --git a/src/filter.coffee b/src/filter.coffee
index 2f89819..8bc4961 100644
--- a/src/filter.coffee
+++ b/src/filter.coffee
@@ -1,44 +1,35 @@
 scorer = require './scorer'
-legacy_scorer = require './legacy'
+pathScorer = require './pathScorer'
+Query = require './query'
 
 pluckCandidates = (a) -> a.candidate
 sortCandidates = (a, b) -> b.score - a.score
-PathSeparator = require('path').sep
 
-module.exports = (candidates, query, {key, maxResults, maxInners, allowErrors, legacy }={}) ->
+module.exports = (candidates, query, options) ->
   scoredCandidates = []
-  spotLeft = if maxInners? and maxInners > 0 then maxInners else candidates.length
 
-  bAllowErrors = !!allowErrors
+  #See also option parsing on main module for default
+  {key, maxResults, maxInners, usePathScoring} = options
+  spotLeft = if maxInners? and maxInners > 0 then maxInners else candidates.length + 1
   bKey = key?
-  prepQuery = scorer.prepQuery(query)
-
-  if(not legacy)
-    for candidate in candidates
-      string = if bKey then candidate[key] else candidate
-      continue unless string
-      score = scorer.score(string, query, prepQuery, bAllowErrors)
-      if score > 0
-        scoredCandidates.push({candidate, score})
-        break unless --spotLeft
-
-  else
-    queryHasSlashes = prepQuery.depth > 0
-    coreQuery = prepQuery.core
-
-    for candidate in candidates
-      string = if key? then candidate[key] else candidate
-      continue unless string
-      score = legacy_scorer.score(string, coreQuery, queryHasSlashes)
-      unless queryHasSlashes
-        score = legacy_scorer.basenameScore(string, coreQuery, score)
-      scoredCandidates.push({candidate, score}) if score > 0
+  scoreProvider = if usePathScoring then pathScorer else scorer
 
+  for candidate in candidates
+    string = if bKey then candidate[key] else candidate
+    continue unless string
+    score = scoreProvider.score(string, query, options)
+    if score > 0
+      scoredCandidates.push({candidate, score})
+      break unless --spotLeft
 
   # Sort scores in descending order
   scoredCandidates.sort(sortCandidates)
 
+  #Extract original candidate
   candidates = scoredCandidates.map(pluckCandidates)
 
+  #Trim to maxResults if specified
   candidates = candidates[0...maxResults] if maxResults?
+
+  #And return
   candidates
diff --git a/src/fuzzaldrin.coffee b/src/fuzzaldrin.coffee
index 947cb44..93cad5a 100644
--- a/src/fuzzaldrin.coffee
+++ b/src/fuzzaldrin.coffee
@@ -1,77 +1,63 @@
-scorer = require './scorer'
-legacy_scorer = require './legacy'
 filter = require './filter'
 matcher = require './matcher'
+scorer = require './scorer'
+pathScorer = require './pathScorer'
+Query = require './query'
+
+preparedQueryCache = null
+defaultPathSeparator = if process and (process.platform is "win32") then '\\' else '/'
 
-PathSeparator = require('path').sep
-prepQueryCache = null
 
 module.exports =
 
-  filter: (candidates, query, options) ->
+  filter: (candidates, query, options = {}) ->
     return [] unless query?.length and candidates?.length
+    options = parseOptions(options, query)
     filter(candidates, query, options)
 
-  prepQuery: (query) ->
-    scorer.prepQuery(query)
-
-#
-# While the API is backward compatible,
-# the following pattern is recommended for speed.
-#
-# query = "..."
-# prepared = fuzzaldrin.prepQuery(query)
-# for candidate in candidates
-#    score = fuzzaldrin.score(candidate, query, prepared)
-#
-# --
-# Alternatively we provide caching of prepQuery to ease direct swap of one library to another.
-#
-
-  score: (string, query, prepQuery, {allowErrors, legacy}={}) ->
+  score: (string, query, options = {}) ->
     return 0 unless string?.length and query?.length
+    options = parseOptions(options, query)
+    if options.usePathScoring
+      return pathScorer.score(string, query, options)
+    else return scorer.score(string, query, options)
 
-    # if prepQuery is given -> use it, else if prepQueryCache match the same query -> use cache, else -> compute & cache
-    prepQuery ?= if prepQueryCache and prepQueryCache.query is query then prepQueryCache else (prepQueryCache = scorer.prepQuery(query))
-
-    if not legacy
-      score = scorer.score(string, query, prepQuery, !!allowErrors)
-    else
-      queryHasSlashes = prepQuery.depth > 0
-      coreQuery = prepQuery.core
-      score = legacy_scorer.score(string, coreQuery, queryHasSlashes)
-      unless queryHasSlashes
-        score = legacy_scorer.basenameScore(string, coreQuery, score)
-
-    score
-
-  match: (string, query, prepQuery, {allowErrors}={}) ->
+  match: (string, query, options = {}) ->
     return [] unless string
     return [] unless query
     return [0...string.length] if string is query
+    options = parseOptions(options, query)
+    return matcher.match(string, query, options)
+
+  wrap: (string, query, options = {}) ->
+    return [] unless string
+    return [] unless query
+    options = parseOptions(options, query)
+    return matcher.wrap(string, query, options)
+
+  prepareQuery: (query, options = {}) ->
+    options = parseOptions(options, query)
+    return options.preparedQuery
 
-    # if prepQuery is given -> use it, else if prepQueryCache match the same query -> use cache, else -> compute & cache
-    prepQuery ?= if prepQueryCache and prepQueryCache.query is query then prepQueryCache else (prepQueryCache = scorer.prepQuery(query))
+#Setup default values
+parseOptions = (options, query) ->
 
-    return [] unless allowErrors or scorer.isMatch(string, prepQuery.core_lw, prepQuery.core_up)
-    string_lw = string.toLowerCase()
-    query_lw = prepQuery.query_lw
+  options.allowErrors ?= false
+  options.usePathScoring ?= true
+  options.useExtensionBonus ?= false
+  options.pathSeparator ?= defaultPathSeparator
+  options.optCharRegEx ?= null
+  options.wrap ?= null
 
-    # Full path results
-    matches = matcher.match(string, string_lw, prepQuery)
+  options.preparedQuery ?=
+    if preparedQueryCache and preparedQueryCache.query is query
+    then preparedQueryCache
+    else (preparedQueryCache = new Query(query, options))
 
-    #if there is no matches on the full path, there should not be any on the base path either.
-    return matches if matches.length is 0
+  return options
 
-    # Is there a base path ?
-    if(string.indexOf(PathSeparator) > -1)
 
-      # Base path results
-      baseMatches = matcher.basenameMatch(string, string_lw, prepQuery)
 
-      # Combine the results, removing duplicate indexes
-      matches = matcher.mergeMatches(matches, baseMatches)
 
-    matches
 
 
diff --git a/src/legacy.coffee b/src/legacy.coffee
deleted file mode 100644
index 43160c4..0000000
--- a/src/legacy.coffee
+++ /dev/null
@@ -1,118 +0,0 @@
-# Original ported from:
-#
-# string_score.js: String Scoring Algorithm 0.1.10
-#
-# http://joshaven.com/string_score
-# https://github.com/joshaven/string_score
-#
-# Copyright (C) 2009-2011 Joshaven Potter <yourtech at gmail.com>
-# Special thanks to all of the contributors listed here https://github.com/joshaven/string_score
-# MIT license: http://www.opensource.org/licenses/mit-license.php
-#
-# Date: Tue Mar 1 2011
-
-PathSeparator = require('path').sep
-
-exports.basenameScore = (string, query, score) ->
-  index = string.length - 1
-  index-- while string[index] is PathSeparator # Skip trailing slashes
-  slashCount = 0
-  lastCharacter = index
-  base = null
-  while index >= 0
-    if string[index] is PathSeparator
-      slashCount++
-      base ?= string.substring(index + 1, lastCharacter + 1)
-    else if index is 0
-      if lastCharacter < string.length - 1
-        base ?= string.substring(0, lastCharacter + 1)
-      else
-        base ?= string
-    index--
-
-  # Basename matches count for more.
-  if base is string
-    score *= 2
-  else if base
-    score += exports.score(base, query)
-
-  # Shallow files are scored higher
-  segmentCount = slashCount + 1
-  depth = Math.max(1, 10 - segmentCount)
-  score *= depth * 0.01
-  score
-
-exports.score = (string, query) ->
-  return 1 if string is query
-
-  # Return a perfect score if the file name itself matches the query.
-  return 1 if queryIsLastPathSegment(string, query)
-
-  totalCharacterScore = 0
-  queryLength = query.length
-  stringLength = string.length
-
-  indexInQuery = 0
-  indexInString = 0
-
-  while indexInQuery < queryLength
-    character = query[indexInQuery++]
-    lowerCaseIndex = string.indexOf(character.toLowerCase())
-    upperCaseIndex = string.indexOf(character.toUpperCase())
-    minIndex = Math.min(lowerCaseIndex, upperCaseIndex)
-    minIndex = Math.max(lowerCaseIndex, upperCaseIndex) if minIndex is -1
-    indexInString = minIndex
-    return 0 if indexInString is -1
-
-    characterScore = 0.1
-
-    # Same case bonus.
-    characterScore += 0.1 if string[indexInString] is character
-
-    if indexInString is 0 or string[indexInString - 1] is PathSeparator
-      # Start of string bonus
-      characterScore += 0.8
-    else if string[indexInString - 1] in ['-', '_', ' ']
-      # Start of word bonus
-      characterScore += 0.7
-
-    # Trim string to after current abbreviation match
-    string = string.substring(indexInString + 1, stringLength)
-
-    totalCharacterScore += characterScore
-
-  queryScore = totalCharacterScore / queryLength
-  ((queryScore * (queryLength / stringLength)) + queryScore) / 2
-
-queryIsLastPathSegment = (string, query) ->
-  if string[string.length - query.length - 1] is PathSeparator
-    string.lastIndexOf(query) is string.length - query.length
-
-
-exports.match = (string, query, stringOffset = 0) ->
-  return [stringOffset...stringOffset + string.length] if string is query
-
-  queryLength = query.length
-  stringLength = string.length
-
-  indexInQuery = 0
-  indexInString = 0
-
-  matches = []
-
-  while indexInQuery < queryLength
-    character = query[indexInQuery++]
-    lowerCaseIndex = string.indexOf(character.toLowerCase())
-    upperCaseIndex = string.indexOf(character.toUpperCase())
-    minIndex = Math.min(lowerCaseIndex, upperCaseIndex)
-    minIndex = Math.max(lowerCaseIndex, upperCaseIndex) if minIndex is -1
-    indexInString = minIndex
-    return [] if indexInString is -1
-
-    matches.push(stringOffset + indexInString)
-
-    # Trim string to after current abbreviation match
-    stringOffset += indexInString + 1
-    string = string.substring(indexInString + 1, stringLength)
-
-  matches
\ No newline at end of file
diff --git a/src/matcher.coffee b/src/matcher.coffee
index bf84b18..e9f9ba1 100644
--- a/src/matcher.coffee
+++ b/src/matcher.coffee
@@ -2,34 +2,123 @@
 # This file should closely follow `scorer` except that it returns an array
 # of indexes instead of a score.
 
-PathSeparator = require('path').sep
-scorer = require './scorer'
+{isMatch, isWordStart, scoreConsecutives, scoreCharacter, scoreAcronyms} = require './scorer'
+
+#
+# Main export
+#
+# Return position of character which matches
+
+exports.match = match = (string, query, options) ->
+
+  {allowErrors, preparedQuery, pathSeparator} = options
+
+  return [] unless allowErrors or isMatch(string, preparedQuery.core_lw, preparedQuery.core_up)
+  string_lw = string.toLowerCase()
+
+  # Full path results
+  matches = computeMatch(string, string_lw, preparedQuery)
+
+  #if there is no matches on the full path, there should not be any on the base path either.
+  return matches if matches.length is 0
+
+  # Is there a base path ?
+  if(string.indexOf(pathSeparator) > -1)
+
+    # Base path results
+    baseMatches = basenameMatch(string, string_lw, preparedQuery, pathSeparator)
+
+    # Combine the results, removing duplicate indexes
+    matches = mergeMatches(matches, baseMatches)
+
+  matches
+
+
+#
+# Wrap
+#
+# Helper around match if you want a string with result wrapped by some delimiter text
+
+exports.wrap = (string, query, options) ->
+
+  if(options.wrap?)
+    {tagClass, tagOpen, tagClose} = options.wrap
+
+  tagClass ?= 'highlight'
+  tagOpen ?= '<strong class="' + tagClass + '">'
+  tagClose ?= '</strong>'
+
+  if string == query
+    return tagOpen + string + tagClose
+
+  #Run get position where a match is found
+  matchPositions = match(string, query, options)
+
+  #If no match return as is
+  if matchPositions.length == 0
+    return string
+
+  #Loop over match positions
+  output = ''
+  matchIndex = -1
+  strPos = 0
+  while ++matchIndex < matchPositions.length
+    matchPos = matchPositions[matchIndex]
+
+    # Get text before the current match position
+    if matchPos > strPos
+      output += string.substring(strPos, matchPos)
+      strPos = matchPos
+
+    # Get consecutive matches to wrap under a single tag
+    while ++matchIndex < matchPositions.length
+      if matchPositions[matchIndex] == matchPos + 1
+        matchPos++
+      else
+        matchIndex--
+        break
+
+    #Get text inside the match, including current character
+    matchPos++
+    if matchPos > strPos
+      output += tagOpen
+      output += string.substring(strPos, matchPos)
+      output += tagClose
+      strPos = matchPos
+
+  #Get string after last match
+  if(strPos < string.length - 1)
+    output += string.substring(strPos)
+
+  #return wrapped text
+  output
+
 
 
-exports.basenameMatch = (subject, subject_lw, prepQuery) ->
+basenameMatch = (subject, subject_lw, preparedQuery, pathSeparator) ->
 
   # Skip trailing slashes
   end = subject.length - 1
-  end-- while subject[end] is PathSeparator
+  end-- while subject[end] is pathSeparator
 
   # Get position of basePath of subject.
-  basePos = subject.lastIndexOf(PathSeparator, end)
+  basePos = subject.lastIndexOf(pathSeparator, end)
 
   #If no PathSeparator, no base path exist.
   return [] if (basePos is -1)
 
   # Get the number of folder in query
-  depth = prepQuery.depth
+  depth = preparedQuery.depth
 
   # Get that many folder from subject
   while(depth-- > 0)
-    basePos = subject.lastIndexOf(PathSeparator, basePos - 1)
+    basePos = subject.lastIndexOf(pathSeparator, basePos - 1)
     return [] if (basePos is -1) #consumed whole subject ?
 
   # Get basePath match
   basePos++
   end++
-  exports.match(subject[basePos ... end], subject_lw[basePos... end], prepQuery, basePos)
+  computeMatch(subject[basePos ... end], subject_lw[basePos... end], preparedQuery, basePos)
 
 
 #
@@ -37,7 +126,7 @@ exports.basenameMatch = (subject, subject_lw, prepQuery) ->
 # (Assume sequences are sorted, matches are sorted by construction.)
 #
 
-exports.mergeMatches = (a, b) ->
+mergeMatches = (a, b) ->
   m = a.length
   n = b.length
 
@@ -70,7 +159,7 @@ exports.mergeMatches = (a, b) ->
 # Align sequence (used for fuzzaldrin.match)
 # Return position of subject characters that match query.
 #
-# Follow closely scorer.doScore.
+# Follow closely scorer.computeScore.
 # Except at each step we record what triggered the best score.
 # Then we trace back to output matched characters.
 #
@@ -80,15 +169,15 @@ exports.mergeMatches = (a, b) ->
 # - no hit miss limit
 
 
-exports.match = (subject, subject_lw, prepQuery, offset = 0) ->
-  query = prepQuery.query
-  query_lw = prepQuery.query_lw
+computeMatch = (subject, subject_lw, preparedQuery, offset = 0) ->
+  query = preparedQuery.query
+  query_lw = preparedQuery.query_lw
 
   m = subject.length
   n = query.length
 
   #this is like the consecutive bonus, but for camelCase / snake_case initials
-  acro_score = scorer.scoreAcronyms(subject, subject_lw, query, query_lw).score
+  acro_score = scoreAcronyms(subject, subject_lw, query, query_lw).score
 
   #Init
   score_row = new Array(n)
@@ -129,14 +218,14 @@ exports.match = (subject, subject_lw, prepQuery, offset = 0) ->
       #Compute a tentative match
       if ( query_lw[j] is si_lw )
 
-        start = scorer.isWordStart(i, subject, subject_lw)
+        start = isWordStart(i, subject, subject_lw)
 
         # Forward search for a sequence of consecutive char
         csc_score = if csc_diag > 0  then csc_diag else
-          scorer.scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start)
+          scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start)
 
         # Determine bonus for matching A[i] with B[j]
-        align = score_diag + scorer.scoreCharacter(i, j, start, acro_score, csc_score)
+        align = score_diag + scoreCharacter(i, j, start, acro_score, csc_score)
 
       #Prepare next sequence & match score.
       score_up = score_row[j] # Current score_up is next run score diag
diff --git a/src/pathScorer.coffee b/src/pathScorer.coffee
new file mode 100644
index 0000000..c392201
--- /dev/null
+++ b/src/pathScorer.coffee
@@ -0,0 +1,132 @@
+{isMatch, computeScore, scoreSize} = require './scorer'
+
+
+tau_depth = 13 # Directory depth at which the full path influence is halved.
+file_coeff = 1.2 # Full path is also penalized for length of basename. This adjust a scale factor for that penalty.
+
+#
+# Main export
+#
+# Manage the logic of testing if there's a match and calling the main scoring function
+# Also manage scoring a path and optional character.
+
+exports.score = (string, query, options) ->
+  {preparedQuery, allowErrors} = options
+  return 0 unless allowErrors or isMatch(string, preparedQuery.core_lw, preparedQuery.core_up)
+  string_lw = string.toLowerCase()
+  score = computeScore(string, string_lw, preparedQuery)
+  score = scorePath(string, string_lw, score, options)
+  return Math.ceil(score)
+
+
+#
+# Score adjustment for path
+#
+
+scorePath = (subject, subject_lw, fullPathScore, options) ->
+  return 0 if fullPathScore is 0
+
+  {preparedQuery, useExtensionBonus, pathSeparator} = options
+
+  # Skip trailing slashes
+  end = subject.length - 1
+  while subject[end] is pathSeparator then end--
+
+  # Get position of basePath of subject.
+  basePos = subject.lastIndexOf(pathSeparator, end)
+  fileLength = end-basePos
+
+  # Get a bonus for matching extension
+  extAdjust = 1.0
+
+  if useExtensionBonus
+    extAdjust += getExtensionScore(subject_lw, preparedQuery.ext, basePos, end, 2)
+    fullPathScore *= extAdjust
+
+  # no basePath, nothing else to compute.
+  return fullPathScore if (basePos is -1)
+
+  # Get the number of folder in query
+  depth = preparedQuery.depth
+
+  # Get that many folder from subject
+  while basePos > -1 and depth-- > 0
+    basePos = subject.lastIndexOf(pathSeparator, basePos - 1)
+
+  # Get basePath score, if BaseName is the whole string, no need to recompute
+  # We still need to apply the folder depth and filename penalty.
+  basePathScore = if (basePos is -1) then fullPathScore else
+    extAdjust * computeScore(subject.slice(basePos + 1, end + 1), subject_lw.slice(basePos + 1, end + 1), preparedQuery)
+
+  # Final score is linear interpolation between base score and full path score.
+  # For low directory depth, interpolation favor base Path then include more of full path as depth increase
+  #
+  # A penalty based on the size of the basePath is applied to fullPathScore
+  # That way, more focused basePath match can overcome longer directory path.
+
+  alpha = 0.5 * tau_depth / ( tau_depth + countDir(subject, end + 1, pathSeparator) )
+  return  alpha * basePathScore + (1 - alpha) * fullPathScore * scoreSize(0, file_coeff * (fileLength))
+
+
+#
+# Count number of folder in a path.
+# (consecutive slashes count as a single directory)
+#
+
+exports.countDir = countDir = (path, end, pathSeparator) ->
+  return 0 if end < 1
+
+  count = 0
+  i = -1
+
+  #skip slash at the start so `foo/bar` and `/foo/bar` have the same depth.
+  while ++i < end and path[i] is pathSeparator
+    continue
+
+  while ++i < end
+    if (path[i] is pathSeparator)
+      count++ #record first slash, but then skip consecutive ones
+      while ++i < end and path[i] is pathSeparator
+        continue
+
+  return count
+
+#
+# Find fraction of extension that is matched by query.
+# For example mf.h prefers myFile.h to myfile.html
+# This need special handling because it give point for not having characters (the `tml` in above example)
+#
+
+exports.getExtension = getExtension = (str) ->
+  pos = str.lastIndexOf(".")
+  if pos < 0 then ""  else  str.substr(pos + 1)
+
+
+getExtensionScore = (candidate, ext, startPos, endPos, maxDepth) ->
+  # startPos is the position of last slash of candidate, -1 if absent.
+
+  return 0 unless ext.length
+
+  # Check that (a) extension exist, (b) it is after the start of the basename
+  pos = candidate.lastIndexOf(".", endPos)
+  return 0 unless pos > startPos # (note that startPos >= -1)
+
+  n = ext.length
+  m = endPos - pos
+
+  # n contain the smallest of both extension length, m the largest.
+  if( m < n)
+    n = m
+    m = ext.length
+
+  #place cursor after dot & count number of matching characters in extension
+  pos++
+  matched = -1
+  while ++matched < n then break if candidate[pos + matched] isnt ext[matched]
+
+  # if nothing found, try deeper for multiple extensions, with some penalty for depth
+  if matched is 0 and maxDepth > 0
+    return 0.9 * getExtensionScore(candidate, ext, startPos, pos - 2, maxDepth - 1)
+
+  # cannot divide by zero because m is the largest extension length and we return if either is 0
+  return  matched / m
diff --git a/src/query.coffee b/src/query.coffee
new file mode 100644
index 0000000..600879b
--- /dev/null
+++ b/src/query.coffee
@@ -0,0 +1,69 @@
+#
+# Query object
+#
+# Allow to reuse some quantities computed from query.
+# Optional char can optionally be specified in the form of a regular expression.
+#
+
+{countDir, getExtension} = require "./pathScorer"
+
+module.exports =
+
+class Query
+  constructor: (query, {optCharRegEx, pathSeparator} = {} ) ->
+    return null unless query and query.length
+
+    @query = query
+    @query_lw = query.toLowerCase()
+    @core = coreChars(query, optCharRegEx)
+    @core_lw = @core.toLowerCase()
+    @core_up = truncatedUpperCase(@core)
+    @depth = countDir(query, query.length, pathSeparator )
+    @ext = getExtension(@query_lw)
+    @charCodes = getCharCodes(@query_lw)
+
+
+#
+# Optional chars
+# Those char improve the score if present, but will not block the match (score=0) if absent.
+
+opt_char_re = /[ _\-:\/\\]/g
+
+coreChars = (query, optCharRegEx = opt_char_re) ->
+  return query.replace(optCharRegEx, '')
+
+#
+# Truncated Upper Case:
+# --------------------
+#
+# A fundamental mechanic is that we are able to keep uppercase and lowercase variant of the strings in sync.
+# For that we assume uppercase and lowercase version of the string have the same length. Of course unicode being unicode there's exceptions.
+# See ftp://ftp.unicode.org/Public/UCD/latest/ucd/SpecialCasing.txt for the list
+#
+# "Stra�e".toUpperCase() -> "STRASSE"
+# truncatedUpperCase("Stra�e") -> "STRASE"
+# iterating over every character, getting uppercase variant and getting first char of that.
+#
+
+truncatedUpperCase = (str) ->
+  upper = ""
+  upper += char.toUpperCase()[0] for char in str
+  return upper
+
+#
+# Get character codes:
+# --------------------
+#
+# Get character codes map for a given string
+#
+
+getCharCodes = (str) ->
+  len = str.length
+  i = -1
+
+  charCodes = []
+  # create map
+  while ++i < len
+    charCodes[str.charCodeAt i] = true
+
+  return charCodes
diff --git a/src/scorer.coffee b/src/scorer.coffee
index 9b1c2bf..3839a5d 100644
--- a/src/scorer.coffee
+++ b/src/scorer.coffee
@@ -8,7 +8,6 @@
 # Copyright (C) 2015 Jean Christophe Roy and contributors
 # MIT License: http://opensource.org/licenses/MIT
 
-PathSeparator = require('path').sep
 
 # Base point for a single character match
 # This balance making patterns VS position and size penalty.
@@ -16,9 +15,7 @@ wm = 150
 
 #Fading function
 pos_bonus = 20 # The character from 0..pos_bonus receive a greater bonus for being at the start of string.
-tau_depth = 13 # Directory depth at which the full path influence is halved.
 tau_size = 85 # Full path length at which the whole match score is halved.
-file_coeff = 1.2 # Full path is also penalized for length of basename. This adjust a scale factor for that penalty.
 
 # Miss count
 # When subject[i] is query[j] we register a hit.
@@ -29,14 +26,6 @@ file_coeff = 1.2 # Full path is also penalized for length of basename. This adju
 # This has a direct influence on worst case scenario benchmark.
 miss_coeff = 0.75 #Max number missed consecutive hit = ceil(miss_coeff*query.length) + 5
 
-#
-# Optional chars
-# Those char improve the score if present, but will not block the match (score=0) if absent.
-
-opt_char_re = /[ _\-:\/\\]/g
-exports.coreChars = coreChars = (query) ->
-  return query.replace(opt_char_re, '')
-
 
 #
 # Main export
@@ -44,31 +33,12 @@ exports.coreChars = coreChars = (query) ->
 # Manage the logic of testing if there's a match and calling the main scoring function
 # Also manage scoring a path and optional character.
 
-exports.score = (string, query, prepQuery = new Query(query), allowErrors = false) ->
-  return 0 unless allowErrors or isMatch(string, prepQuery.core_lw, prepQuery.core_up)
+exports.score = (string, query, options) ->
+  {preparedQuery, allowErrors} = options
+  return 0 unless allowErrors or isMatch(string, preparedQuery.core_lw, preparedQuery.core_up)
   string_lw = string.toLowerCase()
-  score = doScore(string, string_lw, prepQuery)
-  return Math.ceil(basenameScore(string, string_lw, prepQuery, score))
-
-
-#
-# Query object
-#
-# Allow to reuse some quantities computed from query.
-class Query
-  constructor: (query) ->
-    return null unless query?.length
-
-    @query = query
-    @query_lw = query.toLowerCase()
-    @core = coreChars(query)
-    @core_lw = @core.toLowerCase()
-    @core_up = truncatedUpperCase(@core)
-    @depth = countDir(query, query.length)
-
-
-exports.prepQuery = (query) ->
-  return new Query(query)
+  score = computeScore(string, string_lw, preparedQuery)
+  return Math.ceil(score)
 
 
 #
@@ -89,13 +59,13 @@ exports.isMatch = isMatch = (subject, query_lw, query_up) ->
   #foreach char of query
   while ++j < n
 
-    qj_lw = query_lw[j]
-    qj_up = query_up[j]
+    qj_lw = query_lw.charCodeAt j
+    qj_up = query_up.charCodeAt j
 
     # continue walking the subject from where we have left with previous query char
     # until we have found a character that is either lowercase or uppercase query.
     while ++i < m
-      si = subject[i]
+      si = subject.charCodeAt i
       break if si is qj_lw or si is qj_up
 
     # if we passed the last char, query is not in subject
@@ -110,9 +80,9 @@ exports.isMatch = isMatch = (subject, query_lw, query_up) ->
 # Main scoring algorithm
 #
 
-doScore = (subject, subject_lw, prepQuery) ->
-  query = prepQuery.query
-  query_lw = prepQuery.query_lw
+exports.computeScore = computeScore = (subject, subject_lw, preparedQuery) ->
+  query = preparedQuery.query
+  query_lw = preparedQuery.query_lw
 
   m = subject.length
   n = query.length
@@ -157,6 +127,8 @@ doScore = (subject, subject_lw, prepQuery) ->
     score_row[j] = 0
     csc_row[j] = 0
 
+  #TODO: Check if speedup is important on average.
+  #This assume first and last char are correct. Possibly breaking some allowError score.
 
   # Limit the search to the active region
   # for example with query `abc`, subject `____a_bc_ac_c____`
@@ -171,13 +143,26 @@ doScore = (subject, subject_lw, prepQuery) ->
   mm = subject_lw.lastIndexOf(query_lw[n - 1], m)
   if(mm > i) then m = mm + 1
 
+  csc_invalid = true
+
   while ++i < m     #foreach char si of subject
+    si_lw = subject_lw[i]
+
+    # if si_lw is not in query
+    if not preparedQuery.charCodes[si_lw.charCodeAt 0]?
+      # reset csc_row and move to next
+      if csc_invalid isnt true
+        j = -1
+        while ++j < n
+          csc_row[j] = 0
+        csc_invalid = true
+      continue
 
     score = 0
     score_diag = 0
     csc_diag = 0
-    si_lw = subject_lw[i]
     record_miss = true
+    csc_invalid = false
 
     j = -1 #0..n-1
     while ++j < n   #foreach char qj of query
@@ -197,8 +182,8 @@ doScore = (subject, subject_lw, prepQuery) ->
         start = isWordStart(i, subject, subject_lw)
 
         # Forward search for a sequence of consecutive char
-        csc_score = if csc_diag > 0  then csc_diag else scoreConsecutives(subject, subject_lw, query, query_lw, i,
-          j, start)
+        csc_score = if csc_diag > 0  then csc_diag else
+          scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start)
 
         # Determine bonus for matching A[i] with B[j]
         align = score_diag + scoreCharacter(i, j, start, acro_score, csc_score)
@@ -223,7 +208,8 @@ doScore = (subject, subject_lw, prepQuery) ->
       csc_row[j] = csc_score
       score_row[j] = score
 
-
+  # get hightest score so far
+  score = score_row[n - 1]
   return score * sz
 
 #
@@ -237,7 +223,7 @@ exports.isWordStart = isWordStart = (pos, subject, subject_lw) ->
   return true if pos is 0 # match is FIRST char ( place a virtual token separator before first char of string)
   curr_s = subject[pos]
   prev_s = subject[pos - 1]
-  return isSeparator(curr_s) or isSeparator(prev_s) or # match IS or FOLLOW a separator
+  return isSeparator(prev_s) or # match FOLLOW a separator
       (  curr_s isnt subject_lw[pos] and prev_s is subject_lw[pos - 1] ) # match is Capital in camelCase (preceded by lowercase)
 
 
@@ -245,7 +231,7 @@ exports.isWordEnd = isWordEnd = (pos, subject, subject_lw, len) ->
   return true if  pos is len - 1 # last char of string
   curr_s = subject[pos]
   next_s = subject[pos + 1]
-  return isSeparator(curr_s) or isSeparator(next_s) or # match IS or IS FOLLOWED BY a separator
+  return isSeparator(next_s) or # match IS FOLLOWED BY a separator
       ( curr_s is subject_lw[pos] and next_s isnt subject_lw[pos + 1] ) # match is lowercase, followed by uppercase
 
 
@@ -263,7 +249,7 @@ scorePosition = (pos) ->
   else
     return Math.max(100 + pos_bonus - pos, 0)
 
-scoreSize = (n, m) ->
+exports.scoreSize = scoreSize = (n, m) ->
   # Size penalty, use the difference of size (m-n)
   return tau_size / ( tau_size + Math.abs(m - n))
 
@@ -321,7 +307,7 @@ exports.scoreCharacter = scoreCharacter = (i, j, start, acro_score, csc_score) -
 # Forward search for a sequence of consecutive character.
 #
 
-exports.scoreConsecutives = scoreConsecutives = (subject, subject_lw, query, query_lw, i, j, start) ->
+exports.scoreConsecutives = scoreConsecutives = (subject, subject_lw, query, query_lw, i, j, startOfWord) ->
   m = subject.length
   n = query.length
 
@@ -329,7 +315,6 @@ exports.scoreConsecutives = scoreConsecutives = (subject, subject_lw, query, que
   nj = n - j
   k = if mi < nj then mi else nj
 
-  startPos = i #record start position
   sameCase = 0
   sz = 0 #sz will be one more than the last qi is sj
 
@@ -346,7 +331,7 @@ exports.scoreConsecutives = scoreConsecutives = (subject, subject_lw, query, que
   # Acronym should be addressed with acronym context bonus instead of consecutive.
   return 1 + 2 * sameCase if sz is 1
 
-  return scorePattern(sz, n, sameCase, start, isWordEnd(i, subject, subject_lw, m))
+  return scorePattern(sz, n, sameCase, startOfWord, isWordEnd(i, subject, subject_lw, m))
 
 
 #
@@ -399,7 +384,8 @@ exports.scoreAcronyms = scoreAcronyms = (subject, subject_lw, query, query_lw) -
   return emptyAcronymResult unless m > 1 and n > 1
 
   count = 0
-  pos = 0
+  sepCount = 0
+  sumPos = 0
   sameCase = 0
 
   i = -1
@@ -410,118 +396,69 @@ exports.scoreAcronyms = scoreAcronyms = (subject, subject_lw, query, query_lw) -
 
     qj_lw = query_lw[j]
 
-    while ++i < m
+    # Separator will not score point but will continue the prefix when present.
+    # Test that the separator is in the candidate and advance cursor to that position.
+    # If no separator break the prefix
+
+    if isSeparator(qj_lw)
+      i = subject_lw.indexOf(qj_lw, i + 1)
+      if i > -1
+        sepCount++
+        continue
+      else
+        break
 
-      #test if subject match
-      # Only record match that are also start-of-word.
+    # For other characters we search for the first match where subject[i] = query[j]
+    # that also happens to be a start-of-word
+
+    while ++i < m
       if qj_lw is subject_lw[i] and isWordStart(i, subject, subject_lw)
         sameCase++ if ( query[j] is subject[i] )
-        pos += i
+        sumPos += i
         count++
         break
 
-    #all of subject is consumed, stop processing the query.
+    # All of subject is consumed, stop processing the query.
     if i is m then break
 
-  #all of query is consumed.
-  #a single char is not an acronym (also prevent division by 0)
+
+  # Here, all of query is consumed (or we have reached a character not in acronym)
+  # A single character is not an acronym (also prevent division by 0)
   if(count < 2)
     return emptyAcronymResult
 
-  #Acronym are scored as start of word, but not full word
-  score = scorePattern(count, n, sameCase, true, false) # wordStart = true, wordEnd = false
-
-  return new AcronymResult(score, pos / count, count)
-
-
-#----------------------------------------------------------------------
-
-#
-# Score adjustment for path
-#
-
-basenameScore = (subject, subject_lw, prepQuery, fullPathScore) ->
-  return 0 if fullPathScore is 0
-
-
-  # Skip trailing slashes
-  end = subject.length - 1
-  end-- while subject[end] is PathSeparator
-
-  # Get position of basePath of subject.
-  basePos = subject.lastIndexOf(PathSeparator, end)
+  # Acronym are scored as start-of-word
+  # Unless the acronym is a 1:1 match with candidate then it is upgraded to full-word.
+  fullWord = if count is n then isAcronymFullWord(subject, subject_lw, query, count) else false
+  score = scorePattern(count, n, sameCase, true, fullWord)
 
-  #If no PathSeparator, no base path exist.
-  return fullPathScore if (basePos is -1)
-
-  # Get the number of folder in query
-  depth = prepQuery.depth
-
-  # Get that many folder from subject
-  while(depth-- > 0)
-    basePos = subject.lastIndexOf(PathSeparator, basePos - 1)
-    if (basePos is -1) then return fullPathScore #consumed whole subject ?
-
-  # Get basePath score
-  basePos++
-  end++
-  basePathScore = doScore(subject[basePos...end], subject_lw[basePos...end], prepQuery)
-
-  # Final score is linear interpolation between base score and full path score.
-  # For low directory depth, interpolation favor base Path then include more of full path as depth increase
-  #
-  # A penalty based on the size of the basePath is applied to fullPathScore
-  # That way, more focused basePath match can overcome longer directory path.
-
-  alpha = 0.5 * tau_depth / ( tau_depth + countDir(subject, end + 1) )
-  return  alpha * basePathScore + (1 - alpha) * fullPathScore * scoreSize(0, file_coeff * (end - basePos))
+  return new AcronymResult(score, sumPos / count, count + sepCount)
 
 
 #
-# Count number of folder in a path.
-# (consecutive slashes count as a single directory)
+# Test whether there's a 1:1 relationship between query and acronym of candidate.
+# For that to happens
+# (a) All character of query must be matched to an acronym of candidate
+# (b) All acronym of candidate must be matched to a character of query.
 #
+# This method check for (b) assuming (a) has been checked before entering.
 
-exports.countDir = countDir = (path, end) ->
-  return 0 if end < 1
-
+isAcronymFullWord = (subject, subject_lw, query, nbAcronymInQuery) ->
+  m = subject.length
+  n = query.length
   count = 0
-  i = -1
 
-  #skip slash at the start so `foo/bar` and `/foo/bar` have the same depth.
-  while ++i < end and path[i] is PathSeparator
-    continue
-
-  while ++i < end
-    if (path[i] is PathSeparator)
-      count++ #record first slash, but then skip consecutive ones
-      while ++i < end and path[i] is PathSeparator
-        continue
-
-  return count
-
-#
-# Truncated Upper Case:
-# --------------------
-#
-# A fundamental mechanic is that we are able to keep uppercase and lowercase variant of the strings in sync.
-# For that we assume uppercase and lowercase version of the string have the same length
-#
-# Of course unicode being unicode there's exceptions.
-# See ftp://ftp.unicode.org/Public/UCD/latest/ucd/SpecialCasing.txt for the list
-#
-# One common example is 'LATIN SMALL LETTER SHARP S' (U+00DF)
-# "Stra�e".toUpperCase() === "STRASSE" // length goes from 6 char to 7 char
-#
-# Fortunately only uppercase is touched by the exceptions.
-#
-# truncatedUpperCase("Stra�e") returns "STRASE"
-# iterating over every character, getting uppercase variant and getting first char of that.
-#
-# This works for isMatch because we require candidate to contain at least this string.
-# Aka second S of STRASSE is still valid, simply an optional character.
+  # Heuristic:
+  # Assume one acronym every (at most) 12 character on average
+  # This filter out long paths, but then they can match on the filename.
+  if (m > 12 * n) then return false
 
-truncatedUpperCase = (str) ->
-  upper = ""
-  upper += char.toUpperCase()[0] for char in str
-  return upper
\ No newline at end of file
+  i = -1
+  while ++i < m
+    #For each char of subject
+    #Test if we have an acronym, if so increase acronym count.
+    #If the acronym count is more than nbAcronymInQuery (number of non separator char in query)
+    #Then we do not have 1:1 relationship.
+    if isWordStart(i, subject, subject_lw) and ++count > nbAcronymInQuery then return false
+
+  return true
\ No newline at end of file

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-fuzzaldrin-plus.git



More information about the Pkg-javascript-commits mailing list