Index: solr/core/src/java/org/apache/solr/search/ExtendedDismaxQParserPlugin.java
===================================================================
--- solr/core/src/java/org/apache/solr/search/ExtendedDismaxQParserPlugin.java	(revision 1389268)
+++ solr/core/src/java/org/apache/solr/search/ExtendedDismaxQParserPlugin.java	(working copy)
@@ -29,6 +29,7 @@
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.core.StopFilterFactory;
 import org.apache.lucene.analysis.util.TokenFilterFactory;
+import org.apache.lucene.index.Term;
 import org.apache.lucene.queries.function.BoostedQuery;
 import org.apache.lucene.queries.function.FunctionQuery;
 import org.apache.lucene.queries.function.ValueSource;
@@ -37,6 +38,7 @@
 import org.apache.lucene.queryparser.classic.ParseException;
 import org.apache.lucene.queryparser.classic.QueryParser;
 import org.apache.lucene.search.*;
+import org.apache.lucene.search.spans.*;
 import org.apache.solr.analysis.TokenizerChain;
 import org.apache.solr.search.SolrQueryParser.MagicFieldName;
 import org.apache.solr.common.params.DisMaxParams;
@@ -91,17 +93,17 @@
     super(qstr, localParams, params, req);
   }
 
-  /** 
-   * The field names specified by 'qf' that (most) clauses will 
-   * be queried against 
+  /**
+   * The field names specified by 'qf' that (most) clauses will
+   * be queried against
    */
   private Map<String,Float> queryFields;
-   
-  /** 
-   * The field names specified by 'uf' that users are 
+
+  /**
+   * The field names specified by 'uf' that users are
    * allowed to include literally in their query string.  The Float
-   * boost values will be applied automaticly to any clause using that 
-   * field name. '*' will be treated as an alias for any 
+   * boost values will be applied automaticly to any clause using that
+   * field name. '*' will be treated as an alias for any
    * field that exists in the schema. Wildcards are allowed to
    * express dynamicFields.
    */
@@ -121,14 +123,14 @@
   public Query parse() throws ParseException {
     SolrParams localParams = getLocalParams();
     SolrParams params = getParams();
-    
+
     solrParams = SolrParams.wrapDefaults(localParams, params);
 
-    final String minShouldMatch = 
+    final String minShouldMatch =
       DisMaxQParser.parseMinShouldMatch(req.getSchema(), solrParams);
 
     userFields = new UserFields(U.parseFieldBoosts(solrParams.getParams(DMP.UF)));
-    
+
     queryFields = DisMaxQParser.parseQueryFields(req.getSchema(), solrParams);
 
     // Phrase slop array
@@ -137,15 +139,15 @@
     pslop[2] = solrParams.getInt(DisMaxParams.PS2, pslop[0]);
     pslop[3] = solrParams.getInt(DisMaxParams.PS3, pslop[0]);
 
-    
+
     // Boosted phrase of the full query string
-    List<FieldParams> phraseFields = 
+    List<FieldParams> phraseFields =
       U.parseFieldBoostsAndSlop(solrParams.getParams(DMP.PF),0,pslop[0]);
     // Boosted Bi-Term Shingles from the query string
-    List<FieldParams> phraseFields2 = 
+    List<FieldParams> phraseFields2 =
       U.parseFieldBoostsAndSlop(solrParams.getParams(DMP.PF2),2,pslop[2]);
     // Boosted Tri-Term Shingles from the query string
-    List<FieldParams> phraseFields3 = 
+    List<FieldParams> phraseFields3 =
       U.parseFieldBoostsAndSlop(solrParams.getParams(DMP.PF3),3,pslop[3]);
 
     float tiebreaker = solrParams.getFloat(DisMaxParams.TIE, 0.0f);
@@ -176,7 +178,7 @@
         // throw new ParseException("missing query string" );
       }
     }
-    else {     
+    else {
       // There is a valid query string
       // userQuery = partialEscape(U.stripUnbalancedQuotes(userQuery)).toString();
 
@@ -237,9 +239,9 @@
         sb.append(s);
         sb.append(' ');
       }
-      
+
       mainUserQuery = sb.toString();
-      
+
       // For correct lucene queries, turn off mm processing if there
       // were explicit operators (except for AND).
       boolean doMinMatched = (numOR + numNOT + numPluses + numMinuses) == 0;
@@ -252,7 +254,7 @@
         if (stopwords && isEmpty(parsedUserQuery)) {
          // if the query was all stop words, remove none of them
           up.setRemoveStopFilter(true);
-          parsedUserQuery = up.parse(mainUserQuery);          
+          parsedUserQuery = up.parse(mainUserQuery);
         }
       } catch (Exception e) {
         // ignore failure and reparse later after escaping reserved chars
@@ -308,7 +310,6 @@
           parsedUserQuery = t;
         }
       }
-
       query.add(parsedUserQuery, BooleanClause.Occur.MUST);
 
       // sloppy phrase queries for proximity
@@ -317,33 +318,53 @@
       allPhraseFields.addAll(phraseFields2);
       allPhraseFields.addAll(phraseFields3);
 
-      if (allPhraseFields.size() > 0) {
-        // find non-field clauses
-        List<Clause> normalClauses = new ArrayList<Clause>(clauses.size());
-        for (Clause clause : clauses) {
-          if (clause.field != null || clause.isPhrase) continue;
-          // check for keywords "AND,OR,TO"
-          if (clause.isBareWord()) {
-            String s = clause.val.toString();
-            // avoid putting explict operators in the phrase query
-            if ("OR".equals(s) || "AND".equals(s) || "NOT".equals(s) || "TO".equals(s)) continue;
-          }
-          normalClauses.add(clause);
+      /** Needed for phrases and span firsts */
+      // find non-field clauses
+      List<Clause> normalClauses = new ArrayList<Clause>(clauses.size());
+      for (Clause clause : clauses) {
+        if (clause.field != null || clause.isPhrase) continue;
+        // check for keywords "AND,OR,TO"
+        if (clause.isBareWord()) {
+          String s = clause.val.toString();
+          // avoid putting explict operators in the phrase query
+          if ("OR".equals(s) || "AND".equals(s) || "NOT".equals(s) || "TO".equals(s)) continue;
         }
+        normalClauses.add(clause);
+      }
 
+      /** Handle phrase fields */
+      if (allPhraseFields.size() > 0) {
         // full phrase and shingles
         for (FieldParams phraseField: allPhraseFields) {
           Map<String,Float> pf = new HashMap<String,Float>(1);
           pf.put(phraseField.getField(),phraseField.getBoost());
-          addShingledPhraseQueries(query, normalClauses, pf,   
+          addShingledPhraseQueries(query, normalClauses, pf,
           phraseField.getWordGrams(),tiebreaker, phraseField.getSlop());
         }
-        
       }
-    }
 
+      /** Handle span first fields */
+      String[] spanFirstFields = solrParams.getParams(DisMaxParams.SF);
+      if (spanFirstFields != null && spanFirstFields.length > 0) {
+        for (String spanFirstField : spanFirstFields) {
+          String[] parts = spanFirstField.split("\\^|~");
 
+          try {
+            for (Clause clause : normalClauses) {
+              SpanQuery sq = new SpanTermQuery(new Term(parts[0], clause.val));
 
+              Query sfq = new SpanFirstQuery(sq, Integer.parseInt(parts[1]));
+              sfq.setBoost(Float.parseFloat(parts[2]));
+
+              query.add(sfq, BooleanClause.Occur.SHOULD);
+            }
+          } catch (Exception e) {
+            throw new ParseException("Invalid arguments for sf, must be sf=FIELD~DISTANCE^BOOST");
+          }
+        }
+      }
+    }
+
     /* * * Boosting Query * * */
     boostParams = solrParams.getParams(DisMaxParams.BQ);
     //List<Query> boostQueries = U.parseQueryStrings(req, boostParams);
@@ -380,7 +401,6 @@
       }
     }
 
-
     //
     // create a boosted query (scores multiplied by boosts)
     //
@@ -433,7 +453,7 @@
 
   /**
    * Modifies the main query by adding a new optional Query consisting
-   * of shingled phrase queries across the specified clauses using the 
+   * of shingled phrase queries across the specified clauses using the
    * specified field =&gt; boost mappings.
    *
    * @param mainQuery Where the phrase boosting queries will be added
@@ -443,18 +463,18 @@
    * @param tiebreaker tie breker value for the DisjunctionMaxQueries
    * @param slop slop value for the constructed phrases
    */
-  private void addShingledPhraseQueries(final BooleanQuery mainQuery, 
+  private void addShingledPhraseQueries(final BooleanQuery mainQuery,
                                         final List<Clause> clauses,
                                         final Map<String,Float> fields,
                                         int shingleSize,
                                         final float tiebreaker,
-                                        final int slop) 
+                                        final int slop)
     throws ParseException {
-    
-    if (null == fields || fields.isEmpty() || 
-        null == clauses || clauses.size() < shingleSize ) 
+
+    if (null == fields || fields.isEmpty() ||
+        null == clauses || clauses.size() < shingleSize )
       return;
-    
+
     if (0 == shingleSize) shingleSize = clauses.size();
 
     final int goat = shingleSize-1; // :TODO: better name for var?
@@ -479,28 +499,28 @@
       pp.setRemoveStopFilter(true);  // remove stop filter and keep stopwords
 
       /* :TODO: reevaluate using makeDismax=true vs false...
-       * 
-       * The DismaxQueryParser always used DisjunctionMaxQueries for the 
+       *
+       * The DismaxQueryParser always used DisjunctionMaxQueries for the
        * pf boost, for the same reasons it used them for the qf fields.
        * When Yonik first wrote the ExtendedDismaxQParserPlugin, he added
-       * the "makeDismax=false" property to use BooleanQueries instead, but 
+       * the "makeDismax=false" property to use BooleanQueries instead, but
        * when asked why his response was "I honestly don't recall" ...
        *
        * https://issues.apache.org/jira/browse/SOLR-1553?focusedCommentId=12793813#action_12793813
        *
-       * so for now, we continue to use dismax style queries becuse it 
-       * seems the most logical and is back compatible, but we should 
-       * try to figure out what Yonik was thinking at the time (because he 
+       * so for now, we continue to use dismax style queries becuse it
+       * seems the most logical and is back compatible, but we should
+       * try to figure out what Yonik was thinking at the time (because he
        * rarely does things for no reason)
        */
-      pp.makeDismax = true; 
+      pp.makeDismax = true;
 
 
       // minClauseSize is independent of the shingleSize because of stop words
-      // (if they are removed from the middle, so be it, but we need at least 
+      // (if they are removed from the middle, so be it, but we need at least
       // two or there shouldn't be a boost)
-      pp.minClauseSize = 2;  
-      
+      pp.minClauseSize = 2;
+
       // TODO: perhaps we shouldn't use synonyms either...
 
       Query phrase = pp.parse(userPhraseQuery.toString());
@@ -628,7 +648,7 @@
     String raw;  // the raw clause w/o leading/trailing whitespace
   }
 
-  
+
   public List<Clause> splitIntoClauses(String s, boolean ignoreQuote) {
     ArrayList<Clause> lst = new ArrayList<Clause>(4);
     Clause clause;
@@ -641,7 +661,7 @@
     outer: while (pos < end) {
       clause = new Clause();
       disallowUserField = true;
-      
+
       ch = s.charAt(pos);
 
       while (Character.isWhitespace(ch)) {
@@ -649,7 +669,7 @@
         ch = s.charAt(pos);
       }
 
-      start = pos;      
+      start = pos;
 
       if (ch=='+' || ch=='-') {
         clause.must = ch;
@@ -736,7 +756,7 @@
         sb.append(ch);
       }
       clause.val = sb.toString();
- 
+
       if (clause.isPhrase) {
         if (inString != 0) {
           // detected bad quote balancing... retry
@@ -745,7 +765,7 @@
         }
 
         // special syntax in a string isn't special
-        clause.hasSpecialSyntax = false;        
+        clause.hasSpecialSyntax = false;
       } else {
         // an empty clause... must be just a + or - on it's own
         if (clause.val.length() == 0) {
@@ -784,9 +804,9 @@
     return lst;
   }
 
-  /** 
-   * returns a field name or legal field alias from the current 
-   * position of the string 
+  /**
+   * returns a field name or legal field alias from the current
+   * position of the string
    */
   public String getFieldName(String s, int pos, int end) {
     if (pos >= end) return null;
@@ -808,7 +828,7 @@
     boolean isInSchema = getReq().getSchema().getFieldTypeNoEx(fname) != null;
     boolean isAlias = solrParams.get("f."+fname+".qf") != null;
     boolean isMagic = (null != MagicFieldName.get(fname));
-    
+
     return (isInSchema || isAlias || isMagic) ? fname : null;
   }
 
@@ -947,7 +967,7 @@
       a.fields = fieldBoosts;
       aliases.put(field, a);
     }
-    
+
     /**
      * Returns the aliases found for a field.
      * Returns null if there are no aliases for the field
@@ -1116,7 +1136,7 @@
          throw new ParseException("Field aliases lead to a cycle");
        }
     }
-    
+
     private boolean validateField(String field, Set<String> set) {
       if(this.getAlias(field) == null) {
         return false;
@@ -1243,7 +1263,7 @@
     if (q instanceof BooleanQuery && ((BooleanQuery)q).clauses().size()==0) return true;
     return false;
   }
-  
+
   /**
    * Class that encapsulates the input from userFields parameter and can answer whether
    * a field allowed or disallowed as fielded query in the query string
@@ -1258,7 +1278,7 @@
       if (0 == userFieldsMap.size()) {
         userFieldsMap.put("*", null);
       }
-      
+
       // Process dynamic patterns in userFields
       ArrayList<DynamicField> dynUserFields = new ArrayList<DynamicField>();
       ArrayList<DynamicField> negDynUserFields = new ArrayList<DynamicField>();
@@ -1276,30 +1296,30 @@
       negativeDynamicUserFields = negDynUserFields.toArray(new DynamicField[negDynUserFields.size()]);
 //      System.out.println("** userF="+userFieldsMap+", dynUF="+Arrays.toString(dynamicUserFields)+", negDynUF="+Arrays.toString(negativeDynamicUserFields));
     }
-    
+
     /**
      * Is the given field name allowed according to UserFields spec given in the uf parameter?
      * @param fname the field name to examine
      * @return true if the fielded queries are allowed on this field
      */
     public boolean isAllowed(String fname) {
-      boolean res = ((userFieldsMap.containsKey(fname) || isDynField(fname, false)) && 
+      boolean res = ((userFieldsMap.containsKey(fname) || isDynField(fname, false)) &&
           !userFieldsMap.containsKey("-"+fname) &&
           !isDynField(fname, true));
       return res;
     }
-    
+
     private boolean isDynField(String field, boolean neg) {
       return getDynFieldForName(field, neg) == null ? false : true;
     }
-    
+
     private String getDynFieldForName(String f, boolean neg) {
       for( DynamicField df : neg?negativeDynamicUserFields:dynamicUserFields ) {
         if( df.matches( f ) ) return df.wildcard;
       }
       return null;
     }
-    
+
     /**
      * Finds the default user field boost associated with the given field.
      * This is parsed from the uf parameter, and may be specified as wildcards, e.g. *name^2.0 or *^3.0
@@ -1312,7 +1332,7 @@
           userFieldsMap.get(getDynFieldForName(field, false)); // Dynamic field
     }
   }
-  
+
   /* Represents a dynamic field, for easier matching, inspired by same class in IndexSchema */
   static class DynamicField implements Comparable<DynamicField> {
     final static int STARTS_WITH=1;
@@ -1363,7 +1383,7 @@
     public int compareTo(DynamicField other) {
       return other.wildcard.length() - wildcard.length();
     }
-    
+
     public String toString() {
       return this.wildcard;
     }
Index: solr/solrj/src/java/org/apache/solr/common/params/DisMaxParams.java
===================================================================
--- solr/solrj/src/java/org/apache/solr/common/params/DisMaxParams.java	(revision 1366361)
+++ solr/solrj/src/java/org/apache/solr/common/params/DisMaxParams.java	(working copy)
@@ -17,62 +17,65 @@
 
 package org.apache.solr.common.params;
 
-    
 
+
 /**
  * A collection of params used in DisMaxRequestHandler,
  * both for Plugin initialization and for Requests.
  */
 public interface DisMaxParams {
-  
+
   /** query and init param for tiebreaker value */
   public static String TIE = "tie";
-  
+
   /** query and init param for query fields */
   public static String QF = "qf";
-  
+
   /** query and init param for phrase boost fields */
   public static String PF = "pf";
-  
+
   /** query and init param for bigram phrase boost fields */
   public static String PF2 = "pf2";
-  
+
   /** query and init param for trigram phrase boost fields */
   public static String PF3 = "pf3";
-  
+
   /** query and init param for MinShouldMatch specification */
   public static String MM = "mm";
-  
+
   /**
    * query and init param for Phrase Slop value in phrase
    * boost query (in pf fields)
    */
   public static String PS = "ps";
-  
+
   /** default phrase slop for bigram phrases (pf2)  */
   public static String PS2 = "ps2";
-  
+
   /** default phrase slop for bigram phrases (pf3)  */
   public static String PS3 = "ps3";
-    
+
   /**
    * query and init param for phrase Slop value in phrases
    * explicitly included in the user's query string ( in qf fields)
    */
   public static String QS = "qs";
-  
+
   /** query and init param for boosting query */
   public static String BQ = "bq";
-  
+
   /** query and init param for boosting functions */
   public static String BF = "bf";
-  
+
   /**
    * Alternate query (expressed in Solr QuerySyntax)
    * to use if main query (q) is empty
    */
   public static String ALTQ = "q.alt";
-  
+
   /** query and init param for field list */
   public static String GEN = "gen";
+
+  /** query and init param foor spanFirst fields */
+  public static String SF = "sf";
 }
Index: solr/core/src/test/org/apache/solr/search/TestExtendedDismaxParser.java
===================================================================
--- solr/core/src/test/org/apache/solr/search/TestExtendedDismaxParser.java	(revision 1383868)
+++ solr/core/src/test/org/apache/solr/search/TestExtendedDismaxParser.java	(working copy)
@@ -41,7 +41,7 @@
             "name", "The Zapper"));
     assertU(adoc("id", "45", "trait_ss", "Chauvinist",
             "title", "25 star General"));
-    assertU(adoc("id", "46", 
+    assertU(adoc("id", "46",
                  "trait_ss", "Obnoxious",
                  "subject", "Defeated the pacifists op the Gandhi nebula",
                  "t_special", "literal:colon value",
@@ -65,8 +65,8 @@
     // the super classes version
     super.tearDown();
   }
-  
 
+
   public void testLowercaseOperators() {
     assertQ("Upper case operator",
         req("q","Zapp AND Brannigan",
@@ -74,31 +74,31 @@
             "lowercaseOperators", "false",
             "defType","edismax")
         ,"*[count(//doc)=1]");
-    
+
     assertQ("Upper case operator, allow lowercase",
         req("q","Zapp AND Brannigan",
             "qf", "name",
             "lowercaseOperators", "true",
             "defType","edismax")
         ,"*[count(//doc)=1]");
-    
+
     assertQ("Lower case operator, don't allow lowercase operators",
         req("q","Zapp and Brannigan",
             "qf", "name",
-            "q.op", "AND", 
+            "q.op", "AND",
             "lowercaseOperators", "false",
             "defType","edismax")
         ,"*[count(//doc)=0]");
-    
+
     assertQ("Lower case operator, allow lower case operators",
         req("q","Zapp and Brannigan",
             "qf", "name",
-            "q.op", "AND", 
+            "q.op", "AND",
             "lowercaseOperators", "true",
             "defType","edismax")
         ,"*[count(//doc)=1]");
   }
-    
+
   // test the edismax query parser based on the dismax parser
   public void testFocusQueryParser() {
     String allq = "id:[42 TO 51]";
@@ -106,18 +106,18 @@
     String oner = "*[count(//doc)=1]";
     String twor = "*[count(//doc)=2]";
     String nor = "*[count(//doc)=0]";
-    
+
     assertQ("blank q",
         req("q"," ",
             "q.alt",allq,
             "defType","edismax")
         ,allr);
-    
+
     assertQ("expected doc is missing (using un-escaped edismax w/qf)",
-          req("q", "literal:colon", 
+          req("q", "literal:colon",
               "qf", "t_special",
               "defType", "edismax"),
-          "//doc[1]/str[@name='id'][.='46']"); 
+          "//doc[1]/str[@name='id'][.='46']");
 
     assertQ("standard request handler returns all matches",
             req(allq),
@@ -165,16 +165,16 @@
                  ,"q","op")
            , twor
            );
-   
+
    assertQ(req("defType", "edismax", "qf", "name title subject text",
                "q","op"), twor
     );
-   assertQ(req("defType", "edismax", 
+   assertQ(req("defType", "edismax",
                "qf", "name title subject text",
                "q.op", "AND",
                "q","Order op"), oner
     );
-   assertQ(req("defType", "edismax", 
+   assertQ(req("defType", "edismax",
                "qf", "name title subject text",
                "q.op", "OR",
                "q","Order op"), twor
@@ -215,9 +215,9 @@
            "q","the big"), twor
     );
 
-    // test for stopwords not removed   
-    assertQ(req("defType", "edismax", 
-                "qf", "text_sw", 
+    // test for stopwords not removed
+    assertQ(req("defType", "edismax",
+                "qf", "text_sw",
                 "stopwords","false",
                 "q.op","AND",
                 "q","the big"), oner
@@ -226,22 +226,22 @@
     // searching for a literal colon value when clearly not used for a field
     assertQ("expected doc is missing (using standard)",
             req("q", "t_special:literal\\:colon"),
-            "//doc[1]/str[@name='id'][.='46']"); 
+            "//doc[1]/str[@name='id'][.='46']");
     assertQ("expected doc is missing (using escaped edismax w/field)",
-            req("q", "t_special:literal\\:colon", 
+            req("q", "t_special:literal\\:colon",
                 "defType", "edismax"),
-            "//doc[1]/str[@name='id'][.='46']"); 
+            "//doc[1]/str[@name='id'][.='46']");
     assertQ("expected doc is missing (using un-escaped edismax w/field)",
-            req("q", "t_special:literal:colon", 
+            req("q", "t_special:literal:colon",
                 "defType", "edismax"),
-            "//doc[1]/str[@name='id'][.='46']"); 
+            "//doc[1]/str[@name='id'][.='46']");
     assertQ("expected doc is missing (using escaped edismax w/qf)",
-            req("q", "literal\\:colon", 
+            req("q", "literal\\:colon",
                 "qf", "t_special",
                 "defType", "edismax"),
-            "//doc[1]/str[@name='id'][.='46']"); 
+            "//doc[1]/str[@name='id'][.='46']");
     assertQ("expected doc is missing (using un-escaped edismax w/qf)",
-            req("q", "literal:colon", 
+            req("q", "literal:colon",
                 "qf", "t_special",
                 "defType", "edismax"),
             "//doc[1]/str[@name='id'][.='46']");
@@ -273,23 +273,23 @@
 
     assertQ(req("defType","edismax", "mm","0", "q","movies_t:Terminator 100", "qf","movies_t foo_i"),
             twor);
-    
+
     // special psuedo-fields like _query_ and _val_
 
     // special fields (and real field id) should be included by default
-    assertQ(req("defType", "edismax", 
+    assertQ(req("defType", "edismax",
                 "mm", "100%",
                 "fq", "id:51",
                 "q", "_query_:\"{!geofilt d=20 sfield=store pt=12.34,-56.78}\""),
             oner);
     // should also work when explicitly allowed
-    assertQ(req("defType", "edismax", 
+    assertQ(req("defType", "edismax",
                 "mm", "100%",
                 "fq", "id:51",
                 "uf", "id _query_",
                 "q", "_query_:\"{!geofilt d=20 sfield=store pt=12.34,-56.78}\""),
             oner);
-    assertQ(req("defType", "edismax", 
+    assertQ(req("defType", "edismax",
                 "mm", "100%",
                 "fq", "id:51",
                 "uf", "id",
@@ -298,13 +298,13 @@
             oner);
 
     // should fail when prohibited
-    assertQ(req("defType", "edismax", 
+    assertQ(req("defType", "edismax",
                 "mm", "100%",
                 "fq", "id:51",
                 "uf", "* -_query_", // explicitly excluded
                 "q", "_query_:\"{!geofilt d=20 sfield=store pt=12.34,-56.78}\""),
             nor);
-    assertQ(req("defType", "edismax", 
+    assertQ(req("defType", "edismax",
                 "mm", "100%",
                 "fq", "id:51",
                 "uf", "id", // excluded by ommision
@@ -315,7 +315,7 @@
     /** stopword removal in conjunction with multi-word synonyms at query time
      * break this test.
      // multi-word synonyms
-     // remove id:50 which contans the false match      
+     // remove id:50 which contans the false match
     assertQ(req("defType", "edismax", "qf", "text_t", "indent","true", "debugQuery","true",
            "q","-id:50 nyc"), oner
     );
@@ -334,33 +334,33 @@
     ***/
 
   }
-  
+
   public void testBoostQuery() {
     assertQ(
-        req("q", "tekna", "qf", "text_sw", "defType", "edismax", "bq", "id:54^100", "bq", "id:53^10", "fq", "id:[52 TO 54]", "fl", "id,score"), 
+        req("q", "tekna", "qf", "text_sw", "defType", "edismax", "bq", "id:54^100", "bq", "id:53^10", "fq", "id:[52 TO 54]", "fl", "id,score"),
         "//doc[1]/str[@name='id'][.='54']",
         "//doc[2]/str[@name='id'][.='53']",
         "//doc[3]/str[@name='id'][.='52']"
      );
-    
+
     // non-trivial bqs
-    assertQ(req("q", "tekna", 
-                "qf", "text_sw", 
-                "defType", "edismax", 
-                "bq", "(text_sw:blasdfadsf id:54)^100", 
-                "bq", "id:[53 TO 53]^10", 
-                "fq", "id:[52 TO 54]", 
-                "fl", "id,score"), 
+    assertQ(req("q", "tekna",
+                "qf", "text_sw",
+                "defType", "edismax",
+                "bq", "(text_sw:blasdfadsf id:54)^100",
+                "bq", "id:[53 TO 53]^10",
+                "fq", "id:[52 TO 54]",
+                "fl", "id,score"),
             "//doc[1]/str[@name='id'][.='54']",
             "//doc[2]/str[@name='id'][.='53']",
             "//doc[3]/str[@name='id'][.='52']"
             );
 
     // genuine negative boosts are not legal
-    // see SOLR-3823, SOLR-3278, LUCENE-4378 and 
+    // see SOLR-3823, SOLR-3278, LUCENE-4378 and
     // https://wiki.apache.org/solr/SolrRelevancyFAQ#How_do_I_give_a_negative_.28or_very_low.29_boost_to_documents_that_match_a_query.3F
     assertQ(
-        req("q", "tekna", "qf", "text_sw", "defType", "edismax", "bq", "(*:* -id:54)^100", "bq", "id:53^10", "bq", "id:52", "fq", "id:[52 TO 54]", "fl", "id,score"), 
+        req("q", "tekna", "qf", "text_sw", "defType", "edismax", "bq", "(*:* -id:54)^100", "bq", "id:53^10", "bq", "id:52", "fq", "id:[52 TO 54]", "fl", "id,score"),
         "//doc[1]/str[@name='id'][.='53']",
         "//doc[2]/str[@name='id'][.='52']",
         "//doc[3]/str[@name='id'][.='54']"
@@ -371,7 +371,7 @@
     String allr = "*[count(//doc)=10]";
     String oner = "*[count(//doc)=1]";
     String nor = "*[count(//doc)=0]";
-    
+
     // User fields
     // Default is allow all "*"
     // If a list of fields are given, only those are allowed "foo bar"
@@ -381,7 +381,7 @@
     // Also supports "dynamic" field name wildcarding
     assertQ(req("defType","edismax", "q","id:42"),
         oner);
-    
+
     // SOLR-3377 - parens should be allowed immediately before field name
     assertQ(req("defType","edismax", "q","( id:42 )"),
         oner);
@@ -402,102 +402,102 @@
 
     assertQ(req("defType","edismax", "uf","id", "q","id:42"),
         oner);
-    
+
     assertQ(req("defType","edismax", "uf","-*", "q","id:42"),
         nor);
-    
+
     assertQ(req("defType","edismax", "uf","loremipsum", "q","id:42"),
         nor);
-    
+
     assertQ(req("defType","edismax", "uf","* -id", "q","id:42"),
         nor);
-    
+
     assertQ(req("defType","edismax", "uf","* -loremipsum", "q","id:42"),
         oner);
-    
+
     assertQ(req("defType","edismax", "uf","id^5.0", "q","id:42"),
         oner);
-    
+
     assertQ(req("defType","edismax", "uf","*^5.0", "q","id:42"),
         oner);
-    
+
     assertQ(req("defType","edismax", "uf","id^5.0", "q","id:42^10.0"),
         oner);
-    
+
     assertQ(req("defType","edismax", "uf","na*", "q","name:Zapp"),
         oner);
-    
+
     assertQ(req("defType","edismax", "uf","*me", "q","name:Zapp"),
         oner);
-    
+
     assertQ(req("defType","edismax", "uf","* -na*", "q","name:Zapp"),
         nor);
-    
+
     assertQ(req("defType","edismax", "uf","*me -name", "q","name:Zapp"),
         nor);
-    
+
     assertQ(req("defType","edismax", "uf","*ame -*e", "q","name:Zapp"),
         nor);
-    
+
     // Boosts from user fields
     assertQ(req("defType","edismax", "debugQuery","true", "rows","0", "q","id:42"),
         "//str[@name='parsedquery_toString'][.='+id:42']");
-    
+
     assertQ(req("defType","edismax", "debugQuery","true", "rows","0", "uf","*^5.0", "q","id:42"),
         "//str[@name='parsedquery_toString'][.='+id:42^5.0']");
-    
+
     assertQ(req("defType","edismax", "debugQuery","true", "rows","0", "uf","*^2.0 id^5.0 -xyz", "q","name:foo"),
         "//str[@name='parsedquery_toString'][.='+name:foo^2.0']");
-    
+
     assertQ(req("defType","edismax", "debugQuery","true", "rows","0", "uf","i*^5.0", "q","id:42"),
         "//str[@name='parsedquery_toString'][.='+id:42^5.0']");
-    
-    
+
+
     assertQ(req("defType","edismax", "uf","-*", "q","cannons"),
         oner);
-    
+
     assertQ(req("defType","edismax", "uf","* -id", "q","42", "qf", "id"), oner);
-    
+
   }
-  
+
   public void testAliasing() throws Exception {
     String oner = "*[count(//doc)=1]";
     String twor = "*[count(//doc)=2]";
     String nor = "*[count(//doc)=0]";
-    
+
  // Aliasing
     // Single field
     assertQ(req("defType","edismax", "q","myalias:Zapp"),
         nor);
-    
+
     assertQ(req("defType","edismax", "q","myalias:Zapp", "f.myalias.qf","name"),
         oner);
-    
+
     // Multi field
     assertQ(req("defType","edismax", "uf", "myalias", "q","myalias:(Zapp Obnoxious)", "f.myalias.qf","name^2.0 mytrait_ss^5.0", "mm", "50%"),
         oner);
-    
+
     // Multi field
     assertQ(req("defType","edismax", "q","Zapp Obnoxious", "f.myalias.qf","name^2.0 mytrait_ss^5.0"),
         nor);
-    
+
     assertQ(req("defType","edismax", "q","Zapp Obnoxious", "qf","myalias^10.0", "f.myalias.qf","name^2.0 mytrait_ss^5.0"), oner);
     assertQ(req("defType","edismax", "q","Zapp Obnoxious", "qf","myalias^10.0", "f.myalias.qf","name^2.0 trait_ss^5.0"), twor);
     assertQ(req("defType","edismax", "q","Zapp Obnoxious", "qf","myalias^10.0", "f.myalias.qf","name^2.0 trait_ss^5.0", "mm", "100%"), oner);
     assertQ(req("defType","edismax", "q","Zapp Obnoxious", "qf","who^10.0 where^3.0", "f.who.qf","name^2.0", "f.where.qf", "mytrait_ss^5.0"), oner);
-    
+
     assertQ(req("defType","edismax", "q","Zapp Obnoxious", "qf","myalias", "f.myalias.qf","name mytrait_ss", "uf", "myalias"), oner);
-    
+
     assertQ(req("defType","edismax", "uf","who", "q","who:(Zapp Obnoxious)", "f.who.qf", "name^2.0 trait_ss^5.0", "qf", "id"), twor);
     assertQ(req("defType","edismax", "uf","* -name", "q","who:(Zapp Obnoxious)", "f.who.qf", "name^2.0 trait_ss^5.0"), twor);
-    
+
   }
-  
+
   public void testAliasingBoost() throws Exception {
     assertQ(req("defType","edismax", "q","Zapp Pig", "qf","myalias", "f.myalias.qf","name trait_ss^0.5"), "//result/doc[1]/str[@name='id']=42", "//result/doc[2]/str[@name='id']=47");//doc 42 should score higher than 46
     assertQ(req("defType","edismax", "q","Zapp Pig", "qf","myalias^100 name", "f.myalias.qf","trait_ss^0.5"), "//result/doc[1]/str[@name='id']=47", "//result/doc[2]/str[@name='id']=42");//Now the order should be inverse
   }
-  
+
   public void testCyclicAliasing() throws Exception {
     try {
       h.query(req("defType","edismax", "q","ignore_exception", "qf","who", "f.who.qf","name","f.name.qf","who"));
@@ -505,27 +505,27 @@
     } catch (SolrException e) {
       assertTrue(e.getCause().getMessage().contains("Field aliases lead to a cycle"));
     }
-    
+
     try {
       h.query(req("defType","edismax", "q","ignore_exception", "qf","who", "f.who.qf","name","f.name.qf","myalias", "f.myalias.qf","who"));
       fail();
     } catch (SolrException e) {
       assertTrue(e.getCause().getMessage().contains("Field aliases lead to a cycle"));
     }
-    
+
     try {
       h.query(req("defType","edismax", "q","ignore_exception", "qf","field1", "f.field1.qf","field2 field3","f.field2.qf","field4 field5", "f.field4.qf","field5", "f.field5.qf","field6", "f.field3.qf","field6"));
     } catch (SolrException e) {
       fail("This is not cyclic alising");
     }
-    
+
     try {
       h.query(req("defType","edismax", "q","ignore_exception", "qf","field1", "f.field1.qf","field2 field3", "f.field2.qf","field4 field5", "f.field4.qf","field5", "f.field5.qf","field4"));
       fail();
     } catch (SolrException e) {
       assertTrue(e.getCause().getMessage().contains("Field aliases lead to a cycle"));
     }
-    
+
     try {
       h.query(req("defType","edismax", "q","who:(Zapp Pig) ignore_exception", "qf","field1", "f.who.qf","name","f.name.qf","myalias", "f.myalias.qf","who"));
       fail();
@@ -550,7 +550,7 @@
                 "mm", "0"),
             "//*[@numFound='1']",
             "//str[@name='id'][.='142']");
-    
+
     assertQ(req("q", "a_s:xxx AND text_s:yak",
                 "fl", "id",
                 "qf", "a_s b_s",
@@ -569,7 +569,7 @@
             "//*[@numFound='2']",
             "//str[@name='id'][.='144']",
             "//str[@name='id'][.='145']");
-    
+
     assertQ(req("q", "NOT a_s:xxx +text_s:yak",
                 "fl", "id",
                 "qf", "a_s b_s",
@@ -579,7 +579,7 @@
             "//*[@numFound='2']",
             "//str[@name='id'][.='142']",
             "//str[@name='id'][.='144']");
-    
+
     assertQ(req("q", "+bogus:xxx yak",
                 "fl", "id",
                 "qf", "a_s b_s text_s",
@@ -599,17 +599,17 @@
             "//str[@name='id'][.='145']",
             "//str[@name='id'][.='146']");
   }
-  
+
   // test phrase fields including pf2 pf3 and phrase slop
   public void testPfPs() {
-    assertU(adoc("id", "s0", "phrase_sw", "foo bar a b c", "boost_d", "1.0"));    
-    assertU(adoc("id", "s1", "phrase_sw", "foo a bar b c", "boost_d", "2.0"));    
-    assertU(adoc("id", "s2", "phrase_sw", "foo a b bar c", "boost_d", "3.0"));    
-    assertU(adoc("id", "s3", "phrase_sw", "foo a b c bar", "boost_d", "4.0"));    
+    assertU(adoc("id", "s0", "phrase_sw", "foo bar a b c", "boost_d", "1.0"));
+    assertU(adoc("id", "s1", "phrase_sw", "foo a bar b c", "boost_d", "2.0"));
+    assertU(adoc("id", "s2", "phrase_sw", "foo a b bar c", "boost_d", "3.0"));
+    assertU(adoc("id", "s3", "phrase_sw", "foo a b c bar", "boost_d", "4.0"));
     assertU(commit());
 
     assertQ("default order assumption wrong",
-        req("q",   "foo bar", 
+        req("q",   "foo bar",
             "qf",  "phrase_sw",
             "bf",  "boost_d",
             "fl",  "score,*",
@@ -617,47 +617,47 @@
         "//doc[1]/str[@name='id'][.='s3']",
         "//doc[2]/str[@name='id'][.='s2']",
         "//doc[3]/str[@name='id'][.='s1']",
-        "//doc[4]/str[@name='id'][.='s0']"); 
+        "//doc[4]/str[@name='id'][.='s0']");
 
     assertQ("pf not working",
-          req("q",   "foo bar", 
+          req("q",   "foo bar",
               "qf",  "phrase_sw",
               "pf",  "phrase_sw^10",
               "bf",  "boost_d",
               "fl",  "score,*",
               "defType", "edismax"),
           "//doc[1]/str[@name='id'][.='s0']");
-    
+
     assertQ("pf2 not working",
-        req("q",   "foo bar", 
+        req("q",   "foo bar",
             "qf",  "phrase_sw",
             "pf2", "phrase_sw^10",
             "bf",  "boost_d",
             "fl",  "score,*",
             "defType", "edismax"),
-        "//doc[1]/str[@name='id'][.='s0']"); 
+        "//doc[1]/str[@name='id'][.='s0']");
 
     assertQ("pf3 not working",
-        req("q",   "a b bar", 
+        req("q",   "a b bar",
             "qf",  "phrase_sw",
             "pf3", "phrase_sw^10",
             "bf",  "boost_d",
             "fl",  "score,*",
             "defType", "edismax"),
-        "//doc[1]/str[@name='id'][.='s2']"); 
+        "//doc[1]/str[@name='id'][.='s2']");
 
     assertQ("ps not working for pf2",
-        req("q",   "bar foo", 
+        req("q",   "bar foo",
             "qf",  "phrase_sw",
             "pf2", "phrase_sw^10",
             "ps",  "2",
             "bf",  "boost_d",
             "fl",  "score,*",
             "defType", "edismax"),
-        "//doc[1]/str[@name='id'][.='s0']"); 
+        "//doc[1]/str[@name='id'][.='s0']");
 
     assertQ("ps not working for pf3",
-        req("q",   "a bar foo", 
+        req("q",   "a bar foo",
             "qf",  "phrase_sw",
             "pf3", "phrase_sw^10",
             "ps",  "3",
@@ -665,8 +665,8 @@
             "fl",  "score,*",
             "debugQuery",  "true",
             "defType", "edismax"),
-        "//doc[1]/str[@name='id'][.='s1']"); 
-    
+        "//doc[1]/str[@name='id'][.='s1']");
+
     assertQ("ps/ps2/ps3 with default slop overrides not working",
         req("q", "zzzz xxxx cccc vvvv",
             "qf", "phrase_sw",
@@ -684,7 +684,7 @@
         "//str[@name='parsedquery'][contains(.,'phrase_sw:\"cccc vvvv\"~2^22.0')]",
         "//str[@name='parsedquery'][contains(.,'phrase_sw:\"zzzz xxxx\"~3^33.0')]",
         "//str[@name='parsedquery'][contains(.,'phrase_sw:\"xxxx cccc\"~3^33.0')]",
-        "//str[@name='parsedquery'][contains(.,'phrase_sw:\"cccc vvvv\"~3^33.0')]",        
+        "//str[@name='parsedquery'][contains(.,'phrase_sw:\"cccc vvvv\"~3^33.0')]",
         "//str[@name='parsedquery'][contains(.,'phrase_sw:\"zzzz xxxx cccc\"~2^222.0')]",
         "//str[@name='parsedquery'][contains(.,'phrase_sw:\"xxxx cccc vvvv\"~2^222.0')]",
         "//str[@name='parsedquery'][contains(.,'phrase_sw:\"zzzz xxxx cccc\"~3^333.0')]",
@@ -696,13 +696,13 @@
         req("q", "bar foo", "qf", "phrase_sw", "pf2", "phrase_sw^10", "ps2",
             "2", "bf", "boost_d", "fl", "score,*", "defType", "edismax"),
         "//doc[1]/str[@name='id'][.='s0']");
-    
+
     assertQ(
         "Specifying slop in pf2 param not working",
         req("q", "bar foo", "qf", "phrase_sw", "pf2", "phrase_sw~2^10", "bf",
             "boost_d", "fl", "score,*", "defType", "edismax"),
         "//doc[1]/str[@name='id'][.='s0']");
-    
+
     assertQ(
         "Slop in ps2 parameter should override ps",
         req("q", "bar foo", "qf", "phrase_sw", "pf2", "phrase_sw^10", "ps",
@@ -714,13 +714,13 @@
         req("q", "a bar foo", "qf", "phrase_sw", "pf3", "phrase_sw^10", "ps3",
             "3", "bf", "boost_d", "fl", "score,*", "defType", "edismax"),
         "//doc[1]/str[@name='id'][.='s1']");
-    
+
     assertQ(
         "Specifying slop in pf3 param not working",
         req("q", "a bar foo", "qf", "phrase_sw", "pf3", "phrase_sw~3^10", "bf",
             "boost_d", "fl", "score,*", "defType", "edismax"),
         "//doc[1]/str[@name='id'][.='s1']");
-   
+
     assertQ("ps2 should not override slop specified inline in pf2",
         req("q", "zzzz xxxx cccc vvvv",
             "qf", "phrase_sw",
@@ -761,7 +761,7 @@
             "mm", "100%",
             "defType", "edismax")
         , "*[count(//doc)=1]");
-    
+
     // Query string field 'cat_s' for special char / - causes ParseException without patch SOLR-3467
     assertQ("Escaping string with reserved / character",
         req("q", "foo/",
@@ -769,6 +769,20 @@
             "mm", "100%",
             "defType", "edismax")
         , "*[count(//doc)=1]");
-    
+
   }
+
+  /**
+   * check that the sf parameter works properly
+   */
+  @Test
+  public void testSpanFirst() throws Exception {
+    assertQ(
+        req("q", "tekna apple", "qf", "text_sw", "defType", "edismax", "sf", "text_sw~2^5"),
+        "//doc[1]/str[@name='id'][.='52']",
+        "//doc[2]/str[@name='id'][.='53']",
+        "//doc[2]/str[@name='id'][.='49']",
+        "//doc[3]/str[@name='id'][.='54']"
+     );
+  }
 }
