Index: src/test/org/apache/lucene/search/spans/TestSpanRangeQuery.java
===================================================================
--- src/test/org/apache/lucene/search/spans/TestSpanRangeQuery.java	(revision 0)
+++ src/test/org/apache/lucene/search/spans/TestSpanRangeQuery.java	(revision 0)
@@ -0,0 +1,240 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.search.spans;
+
+import junit.framework.TestCase;
+import org.apache.lucene.analysis.WhitespaceAnalyzer;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.index.TermEnum;
+import org.apache.lucene.search.ConstantScoreRangeQuery;
+import org.apache.lucene.search.Hits;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.RangeQuery;
+import org.apache.lucene.search.spans.SpanNearQuery;
+import org.apache.lucene.search.spans.SpanQuery;
+import org.apache.lucene.search.spans.SpanTermQuery;
+import org.apache.lucene.search.spans.Spans;
+import org.apache.lucene.store.RAMDirectory;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * Test for SpanRangeQuery class
+ */
+public class TestSpanRangeQuery extends TestCase {
+  private static final String[] FIELD_DATA = { "aa bb cc dd", "aa dd", "dd gg", "yy zz" };
+  private static final String   FIELD_NAME = "text";
+  private static Set            termSet;
+
+  static {
+    termSet = new HashSet();
+
+    for (int i = 0; i < FIELD_DATA.length; i++) {
+      String[] words = FIELD_DATA[i].split("\\s+");
+
+      for (int j = 0; j < words.length; j++) {
+        Term term = new Term(FIELD_NAME, words[j]);
+
+        if (!termSet.contains(term)) {
+          termSet.add(term);
+        }
+      }
+    }
+  }
+
+  private IndexReader   reader;
+  private IndexSearcher searcher;
+
+  public TestSpanRangeQuery(String name) throws IOException {
+    super(name);
+  }
+
+  protected void setUp() throws Exception {
+    super.setUp();
+
+    RAMDirectory directory = new RAMDirectory();
+    IndexWriter  writer    = new IndexWriter(directory, new WhitespaceAnalyzer(), true);
+
+    for (int i = 0; i < FIELD_DATA.length; i++) {
+      Document doc = new Document();
+
+      doc.add(new Field(FIELD_NAME, FIELD_DATA[i], Field.Store.YES, Field.Index.TOKENIZED));
+      writer.addDocument(doc);
+    }
+
+    writer.close();
+    searcher = new IndexSearcher(directory);
+    reader   = IndexReader.open(directory);
+  }
+
+  protected void tearDown() throws Exception {
+    super.tearDown();
+
+    if (searcher != null) {
+      searcher.close();
+      searcher = null;
+    }
+  }
+
+  public void testEquivalenceOfConstructors() {
+    SpanRangeQuery srq1 = new SpanRangeQuery(new ConstantScoreRangeQuery(FIELD_NAME, "yy", "zz", true, true));
+
+    assertTrue(srq1 != null);
+
+    RangeQuery     rq   = new RangeQuery(new Term(FIELD_NAME, "yy"), new Term(FIELD_NAME, "zz"), true);
+    SpanRangeQuery srq2 = new SpanRangeQuery(rq);
+
+    assertTrue(srq2 != null);
+    assertEquals(srq1, srq2);
+
+    SpanRangeQuery srq3 = new SpanRangeQuery(FIELD_NAME, "yy", "zz", true);
+
+    assertEquals(srq1, srq3);
+    assertEquals(srq2, srq3);
+  }
+
+  public void testTermEnum() throws IOException {
+    TermEnum enumerator      = getIndexReader().terms(new Term(FIELD_NAME, ""));
+    Set      observedTermSet = new HashSet();
+
+    do {
+      Term t = enumerator.term();
+
+      if (!observedTermSet.contains(t)) {
+        observedTermSet.add(t);
+      }
+    } while (enumerator.next());
+
+    enumerator.close();
+    assertEquals(termSet, observedTermSet);
+  }
+
+  public void testGetField() {
+    SpanRangeQuery srq = getSpanRangeQuery("aa", "bb");
+
+    assertEquals(FIELD_NAME, srq.getField());
+  }
+
+  public void testRewrite() throws IOException {
+    SpanRangeQuery srq       = getSpanRangeQuery("aa", "bb");
+    Query          rewritten = srq.rewrite(getIndexReader());
+
+    assertTrue(rewritten instanceof Query);
+  }
+
+  public void testExtractTerms() throws IOException {
+    SpanRangeQuery srq           = getSpanRangeQuery("aa", "cc");
+    Set            expectedTerms = new HashSet();
+    String[]       words         = { "aa", "bb", "cc" };
+
+    for (int i = 0; i < words.length; i++) {
+      expectedTerms.add(new Term(FIELD_NAME, words[i]));
+    }
+
+    Query rewritten = srq.rewrite(getIndexReader());
+    Set   terms     = new HashSet();
+
+    rewritten.extractTerms(terms);
+
+    Iterator iter = terms.iterator();
+
+    while (iter.hasNext()) {
+      Term term = (Term) iter.next();
+    }
+
+    assertEquals("Extracted terms", expectedTerms, terms);
+  }
+
+  /*
+   * Data: { "aa bb cc dd", "aa dd", "dd gg", "yy zz" }; See TestSpans in the
+   * Lucene source tree (src/test); specifically, the method
+   * testSpanOrDouble().
+   */
+  public void testDumpSpans() throws IOException {
+    SpanRangeQuery srq   = getSpanRangeQuery("aa", "cc");
+    Spans          spans = srq.getSpans(getIndexReader());
+
+    while (spans.next()) {
+      System.out.println("<span doc=\"" + spans.doc() + "\" start=\"" + spans.start() + "\" end=\"" + spans.end()
+                         + "\"/>");
+    }
+
+    assertTrue(true);
+  }
+
+  public void testGetSpans() throws IOException {
+    SpanRangeQuery srq   = getSpanRangeQuery("aa", "cc");
+    Spans          spans = srq.getSpans(getIndexReader());
+
+    // Matches document 0 at "aa"
+    assertTrue("first range", spans.next());
+    assertEquals("first doc", 0, spans.doc());
+    assertEquals("first start", 0, spans.start());
+    assertEquals("first end", 1, spans.end());
+
+    // Matches document 0 at "bb"
+    assertTrue("second range", spans.next());
+    assertEquals("second doc", 0, spans.doc());
+    assertEquals("second start", 1, spans.start());
+    assertEquals("second end", 2, spans.end());
+
+    // Matches document 0 at "cc"
+    assertTrue("third range", spans.next());
+    assertEquals("third doc", 0, spans.doc());
+    assertEquals("third start", 2, spans.start());
+    assertEquals("third end", 3, spans.end());
+
+    // Matches document 1 at "aa"
+    assertTrue("fourth range", spans.next());
+    assertEquals("fourth doc", 1, spans.doc());
+    assertEquals("fourth start", 0, spans.start());
+    assertEquals("fourth end", 1, spans.end());
+
+    // Doesn't match any other document
+    assertFalse("fifth range", spans.next());
+  }
+
+  public void testSearch() throws IOException {
+    SpanTermQuery  spanTermQuery  = new SpanTermQuery(new Term(FIELD_NAME, "aa"));
+    SpanRangeQuery spanRangeQuery = new SpanRangeQuery(new ConstantScoreRangeQuery(FIELD_NAME, "cc", "dd", true, true));
+    SpanNearQuery  query          = new SpanNearQuery(new SpanQuery[] { spanTermQuery, spanRangeQuery }, 0, true);
+    Hits           hits           = searcher.search(query);
+
+    assertEquals(1, hits.length());
+
+    Document hitDoc    = hits.doc(0);
+    String[] fieldVals = hitDoc.getValues(FIELD_NAME);
+
+    assertEquals(FIELD_DATA[1], fieldVals[0]);
+  }
+
+  protected SpanRangeQuery getSpanRangeQuery(String lower, String upper) {
+    return new SpanRangeQuery(new ConstantScoreRangeQuery(FIELD_NAME, lower, upper, true, true));
+  }
+
+  protected IndexReader getIndexReader() throws IOException {
+    return reader;
+  }
+}
+
Index: src/java/org/apache/lucene/search/spans/SpanRangeQuery.java
===================================================================
--- src/java/org/apache/lucene/search/spans/SpanRangeQuery.java	(revision 0)
+++ src/java/org/apache/lucene/search/spans/SpanRangeQuery.java	(revision 0)
@@ -0,0 +1,215 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.search.spans;
+
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.ConstantScoreRangeQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.RangeQuery;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.spans.SpanOrQuery;
+import org.apache.lucene.search.spans.SpanQuery;
+import org.apache.lucene.search.spans.SpanTermQuery;
+import org.apache.lucene.search.spans.Spans;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * A SpanQuery version of {@link ConstantScoreRangeQuery} allowing such queries
+ * to be nested within other SpanQuery subclasses.
+ */
+public class SpanRangeQuery extends SpanQuery {
+  private static final long serialVersionUID = 4764863263582550266L;
+  private volatile int      hashCode         = 0;
+  private String            fieldName;
+  private boolean           isInclusive;
+  private String            lower;
+  private String            upper;
+
+  /**
+   * Get an instance based on an instance of ConstantScoreRangeQuery
+   */
+  public SpanRangeQuery(ConstantScoreRangeQuery query) {
+    this.lower       = query.getLowerVal();
+    this.upper       = query.getUpperVal();
+    this.fieldName   = query.getField().intern();
+    this.isInclusive = query.includesLower() && query.includesUpper();
+  }
+
+  /**
+   * Get an instance based on an instance of RangeQuery
+   */
+  public SpanRangeQuery(RangeQuery query) {
+    this.fieldName   = query.getField().intern();
+    this.lower       = query.getLowerTerm().text();
+    this.upper       = query.getUpperTerm().text();
+    this.isInclusive = query.isInclusive();
+  }
+
+  /**
+   * Get an instance based on the components of the range query
+   */
+  public SpanRangeQuery(String fieldName, String lower, String upper, boolean isInclusive) {
+    this.fieldName   = fieldName.intern();
+    this.lower       = lower;
+    this.upper       = upper;
+    this.isInclusive = isInclusive;
+  }
+
+  /**
+   * Returns a Query instance which supports the operations
+   * {@link #getTerms()} and {@link #extractTerms(Set)}. There is probably a
+   * much better way to do this, but so far I do not see how
+   * ConstantScoreQuery supports those operations.
+   */
+  public Query rewrite(IndexReader reader) throws IOException {
+    Term       lowerTerm = new Term(fieldName, lower);
+    Term       upperTerm = new Term(fieldName, upper);
+    RangeQuery rq        = new RangeQuery(lowerTerm, upperTerm, isInclusive);
+
+    rq.setBoost(getBoost());
+
+    BooleanQuery    booleanQuery = (BooleanQuery) rq.rewrite(reader);
+    BooleanClause[] clauses      = booleanQuery.getClauses();
+    SpanQuery[]     spanClauses  = new SpanQuery[clauses.length];
+
+    for (int i = 0; i < clauses.length; i++) {
+      TermQuery     query         = (TermQuery) clauses[i].getQuery();
+      Term          term          = query.getTerm();
+      SpanTermQuery spanTermQuery = new SpanTermQuery(term);
+
+      spanTermQuery.setBoost(query.getBoost());
+      spanClauses[i] = spanTermQuery;
+    }
+
+    SpanOrQuery spanOrQuery = new SpanOrQuery(spanClauses);
+
+    return spanOrQuery;
+  }
+
+  public String getField() {
+    return fieldName;
+  }
+
+  /**
+   * Delegate to the {@link #extractTerms(Set)} method.
+   */
+  public Collection getTerms() {
+    Set termSet = new TreeSet();
+
+    extractTerms(termSet);
+
+    return termSet;
+  }
+
+  /**
+   * Expert: adds all terms occuring in this query to the terms set. Only
+   * works if this query is in its {@link #rewrite rewritten} form. If this
+   * query has been rewritten, then this call would be made to the resulting
+   * BooleanQuery instance, and this class will not ever have this method
+   * invoked. At least, that's the plan.
+   */
+  public void extractTerms(Set terms) {
+    throw new UnsupportedOperationException();
+  }
+
+  /**
+   * Rewrite this query. For each BooleanClause within the resulting
+   * BooleanQuery, extract the TermQuery and construct a new SpanTermQuery.
+   * From the resulting array, build a new SpanOrQuery and call its getSpans()
+   * method. (It's a full court shot, but it just might go in ;-)
+   */
+  public Spans getSpans(IndexReader reader) throws IOException {
+    SpanOrQuery spanOrQuery = (SpanOrQuery) rewrite(reader);
+
+    return spanOrQuery.getSpans(reader);
+  }
+
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+
+    if (!(o instanceof SpanRangeQuery)) {
+      return false;
+    }
+
+    SpanRangeQuery srq = (SpanRangeQuery) o;
+
+    return lower.equals(srq.lower) && upper.equals(srq.upper) && (fieldName == srq.fieldName)
+           && (isInclusive == srq.isInclusive);
+  }
+
+  public int hashCode() {
+    if (0 == hashCode) {
+      int result = 17;
+
+      result   += 37 * result + lower.hashCode();
+      result   += 37 * result + upper.hashCode();
+      result   += 37 * result + fieldName.hashCode();
+      result   += 37 * result + (isInclusive
+                                 ? 1
+                                 : 0);
+      hashCode = result;
+    }
+
+    return hashCode;
+  }
+
+  public String toString(String field) {
+    StringBuffer buffer = new StringBuffer();
+
+    buffer.append("SpanRangeQuery(");
+    buffer.append("lower = " + lower + ", ");
+    buffer.append("upper = " + upper + ", ");
+    buffer.append("fieldName = " + fieldName + ", ");
+    buffer.append("isInclusive = " + isInclusive);
+    buffer.append(")");
+
+    return buffer.toString();
+  }
+
+  public String getLower() {
+    return lower;
+  }
+
+  public void setLower(String lower) {
+    this.lower = lower;
+  }
+
+  public String getUpper() {
+    return upper;
+  }
+
+  public void setUpper(String upper) {
+    this.upper = upper;
+  }
+
+  public boolean isInclusive() {
+    return isInclusive;
+  }
+
+  public void setInclusive(boolean isInclusive) {
+    this.isInclusive = isInclusive;
+  }
+}
+
