diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 6173ece..c9d5503 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -44,6 +44,11 @@ New Features
   the suggester to ignore such variations. (Robert Muir, Sudarshan
   Gaikaiwari, Mike McCandless)
 
+* LUCENE-4491: AnalyzingSuggester now accepts dedicated surface forms
+  that can differ from the suggest key. Custom suggesters can also 
+  extend LookupResult classes to return custom suggestions including
+  entity meta-data. (Simon Willnauer)
+
 API Changes
 
 * LUCENE-4399: Deprecated AppendingCodec. Lucene's term dictionaries
diff --git a/lucene/suggest/src/java/org/apache/lucene/search/spell/HighFrequencyDictionary.java b/lucene/suggest/src/java/org/apache/lucene/search/spell/HighFrequencyDictionary.java
index e9f908d..b81d41c 100644
--- a/lucene/suggest/src/java/org/apache/lucene/search/spell/HighFrequencyDictionary.java
+++ b/lucene/suggest/src/java/org/apache/lucene/search/spell/HighFrequencyDictionary.java
@@ -59,7 +59,7 @@ public class HighFrequencyDictionary implements Dictionary {
     return new HighFrequencyIterator();
   }
 
-  final class HighFrequencyIterator implements TermFreqIterator {
+  final class HighFrequencyIterator extends TermFreqIterator {
     private final BytesRef spare = new BytesRef();
     private final TermsEnum termsEnum;
     private int minNumDocs;
diff --git a/lucene/suggest/src/java/org/apache/lucene/search/spell/TermFreqIterator.java b/lucene/suggest/src/java/org/apache/lucene/search/spell/TermFreqIterator.java
index 13aba48..e9c7240 100644
--- a/lucene/suggest/src/java/org/apache/lucene/search/spell/TermFreqIterator.java
+++ b/lucene/suggest/src/java/org/apache/lucene/search/spell/TermFreqIterator.java
@@ -20,22 +20,49 @@ package org.apache.lucene.search.spell;
 import java.io.IOException;
 import java.util.Comparator;
 
+import org.apache.lucene.search.suggest.Lookup;
+import org.apache.lucene.search.suggest.analyzing.AnalyzingSuggester;
+import org.apache.lucene.util.Attribute;
+import org.apache.lucene.util.AttributeSource;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.BytesRefIterator;
+import org.apache.lucene.util.AttributeSource.AttributeFactory;
 
 /**
  * Interface for enumerating term,weight pairs.
  */
-public interface TermFreqIterator extends BytesRefIterator {
+public abstract class TermFreqIterator extends AttributeSource implements BytesRefIterator {
+  
+  /**
+   * A TokenStream using the default attribute factory.
+   */
+  protected TermFreqIterator() {
+    super();
+  }
+  
+  /**
+   * A {@link TermFreqIterator} that uses the same attributes as the supplied one.
+   */
+  protected TermFreqIterator(AttributeSource input) {
+    super(input);
+  }
+  
+  /**
+   * A {@link TermFreqIterator} using the supplied AttributeFactory for creating new {@link Attribute} instances.
+   */
+  protected TermFreqIterator(AttributeFactory factory) {
+    super(factory);
+  }
 
   /** A term's weight, higher numbers mean better suggestions. */
-  public long weight();
+  public abstract long weight();
+  
   
   /**
    * Wraps a BytesRefIterator as a TermFreqIterator, with all weights
    * set to <code>1</code>
    */
-  public static class TermFreqIteratorWrapper implements TermFreqIterator {
+  public static class TermFreqIteratorWrapper extends TermFreqIterator {
     private BytesRefIterator wrapped;
     
     /** 
@@ -58,5 +85,6 @@ public interface TermFreqIterator extends BytesRefIterator {
     public Comparator<BytesRef> getComparator() {
       return wrapped.getComparator();
     }
+
   }
 }
diff --git a/lucene/suggest/src/java/org/apache/lucene/search/suggest/BufferingTermFreqIteratorWrapper.java b/lucene/suggest/src/java/org/apache/lucene/search/suggest/BufferingTermFreqIteratorWrapper.java
index d686ba3..862ba02 100644
--- a/lucene/suggest/src/java/org/apache/lucene/search/suggest/BufferingTermFreqIteratorWrapper.java
+++ b/lucene/suggest/src/java/org/apache/lucene/search/suggest/BufferingTermFreqIteratorWrapper.java
@@ -27,7 +27,7 @@ import org.apache.lucene.util.BytesRef;
  * This wrapper buffers incoming elements.
  * @lucene.experimental
  */
-public class BufferingTermFreqIteratorWrapper implements TermFreqIterator {
+public class BufferingTermFreqIteratorWrapper extends TermFreqIterator {
   // TODO keep this for now
   /** buffered term entries */
   protected BytesRefList entries = new BytesRefList();
@@ -70,6 +70,4 @@ public class BufferingTermFreqIteratorWrapper implements TermFreqIterator {
   public Comparator<BytesRef> getComparator() {
     return comp;
   }
-
- 
 }
diff --git a/lucene/suggest/src/java/org/apache/lucene/search/suggest/FileDictionary.java b/lucene/suggest/src/java/org/apache/lucene/search/suggest/FileDictionary.java
index c032b1f..abc898c 100644
--- a/lucene/suggest/src/java/org/apache/lucene/search/suggest/FileDictionary.java
+++ b/lucene/suggest/src/java/org/apache/lucene/search/suggest/FileDictionary.java
@@ -61,7 +61,7 @@ public class FileDictionary implements Dictionary {
     return new FileIterator();
   }
 
-  final class FileIterator implements TermFreqIterator {
+  final class FileIterator extends TermFreqIterator {
     private long curFreq;
     private final BytesRef spare = new BytesRef();
     
diff --git a/lucene/suggest/src/java/org/apache/lucene/search/suggest/Lookup.java b/lucene/suggest/src/java/org/apache/lucene/search/suggest/Lookup.java
index 88e4f48..1d26ff6 100644
--- a/lucene/suggest/src/java/org/apache/lucene/search/suggest/Lookup.java
+++ b/lucene/suggest/src/java/org/apache/lucene/search/suggest/Lookup.java
@@ -36,7 +36,7 @@ public abstract class Lookup {
   /**
    * Result of a lookup.
    */
-  public static final class LookupResult implements Comparable<LookupResult> {
+  public static class LookupResult implements Comparable<LookupResult> {
     /** the key's text */
     public final CharSequence key;
     /** the key's weight */
diff --git a/lucene/suggest/src/java/org/apache/lucene/search/suggest/SortedTermFreqIteratorWrapper.java b/lucene/suggest/src/java/org/apache/lucene/search/suggest/SortedTermFreqIteratorWrapper.java
index 3aa4b79..5028da8 100644
--- a/lucene/suggest/src/java/org/apache/lucene/search/suggest/SortedTermFreqIteratorWrapper.java
+++ b/lucene/suggest/src/java/org/apache/lucene/search/suggest/SortedTermFreqIteratorWrapper.java
@@ -35,7 +35,7 @@ import org.apache.lucene.util.IOUtils;
  * This wrapper buffers incoming elements and makes sure they are sorted based on given comparator.
  * @lucene.experimental
  */
-public class SortedTermFreqIteratorWrapper implements TermFreqIterator {
+public class SortedTermFreqIteratorWrapper extends TermFreqIterator {
   
   private final TermFreqIterator source;
   private File tempInput;
@@ -188,5 +188,5 @@ public class SortedTermFreqIteratorWrapper implements TermFreqIterator {
     scratch.length -= 8; // sep + long
     return tmpInput.readLong();
   }
-  
+
 }
diff --git a/lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/AnalyzingSuggester.java b/lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/AnalyzingSuggester.java
index 9f98814..3b4d969 100644
--- a/lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/AnalyzingSuggester.java
+++ b/lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/AnalyzingSuggester.java
@@ -38,6 +38,7 @@ import org.apache.lucene.store.ByteArrayDataInput;
 import org.apache.lucene.store.ByteArrayDataOutput;
 import org.apache.lucene.store.InputStreamDataInput;
 import org.apache.lucene.store.OutputStreamDataOutput;
+import org.apache.lucene.search.suggest.Lookup.LookupResult; // for javadoc
 import org.apache.lucene.util.ArrayUtil;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.CharsRef;
@@ -312,6 +313,18 @@ public class AnalyzingSuggester extends Lookup {
     }
   }
   
+  /**
+   * Builds up a new internal {@link Lookup} representation based on the given
+   * {@link TermFreqIterator}. {@link AnalyzingSuggester} supports
+   * {@link SurfaceFormAttribute} on the {@link TermFreqIterator} to obtain a
+   * surface form that differs from the suggestion key provided by the
+   * {@link TermFreqIterator}. If the
+   * {@link SurfaceFormAttribute#getSurfaceForm()} returns <code>null</code> the
+   * key is used as the surface form instead.
+   * <p>
+   *  Note: This implementation will resort the data internally.
+   * </p>
+   * */
   @Override
   public void build(TermFreqIterator iterator) throws IOException {
     String prefix = getClass().getSimpleName();
@@ -330,11 +343,17 @@ public class AnalyzingSuggester extends Lookup {
     try {
       ByteArrayDataOutput output = new ByteArrayDataOutput(buffer);
       BytesRef surfaceForm;
-      while ((surfaceForm = iterator.next()) != null) {
+      BytesRef key;
+      final SurfaceFormAttribute surfaceFormAttribute = iterator.addAttribute(SurfaceFormAttribute.class);
+      while ((key = iterator.next()) != null) {
 
         // Analyze surface form:
-        TokenStream ts = indexAnalyzer.tokenStream("", new StringReader(surfaceForm.utf8ToString()));
-
+        TokenStream ts = indexAnalyzer.tokenStream("", new StringReader(key.utf8ToString()));
+        surfaceForm = surfaceFormAttribute.getSurfaceForm();
+        if (surfaceForm == null) {
+          surfaceForm = key;
+        }
+      
         // Create corresponding automaton: labels are bytes
         // from each analyzed token, with byte 0 used as
         // separator between tokens:
@@ -445,7 +464,6 @@ public class AnalyzingSuggester extends Lookup {
         analyzed.bytes[analyzed.length] = 0;
         analyzed.bytes[analyzed.length+1] = (byte) dedup;
         analyzed.length += 2;
-
         Util.toIntsRef(analyzed, scratchInts);
         //System.out.println("ADD: " + scratchInts + " -> " + cost + ": " + surface.utf8ToString());
         builder.add(scratchInts, outputs.newPair(cost, BytesRef.deepCopyOf(surface)));
@@ -530,9 +548,9 @@ public class AnalyzingSuggester extends Lookup {
       FST.Arc<Pair<Long,BytesRef>> scratchArc = new FST.Arc<Pair<Long,BytesRef>>();
 
       List<LookupResult> results = new ArrayList<LookupResult>();
+      final BytesRef keyScratch = new BytesRef(key);
 
       if (exactFirst) {
-
         int count = 0;
         for (FSTUtil.Path<Pair<Long,BytesRef>> path : prefixPaths) {
           if (fst.findTargetArc(END_BYTE, path.fstNode, scratchArc, bytesReader) != null) {
@@ -561,7 +579,7 @@ public class AnalyzingSuggester extends Lookup {
         }
 
         MinResult<Pair<Long,BytesRef>> completions[] = searcher.search();
-
+      
         // NOTE: this is rather inefficient: we enumerate
         // every matching "exactly the same analyzed form"
         // path, and then do linear scan to see if one of
@@ -575,10 +593,8 @@ public class AnalyzingSuggester extends Lookup {
         // nodes we have and the
         // maxSurfaceFormsPerAnalyzedForm:
         for(MinResult<Pair<Long,BytesRef>> completion : completions) {
-          spare.grow(completion.output.output2.length);
-          UnicodeUtil.UTF8toUTF16(completion.output.output2, spare);
-          if (CHARSEQUENCE_COMPARATOR.compare(spare, key) == 0) {
-            results.add(new LookupResult(spare.toString(), decodeWeight(completion.output.output1)));
+          if (isExactMatch(keyScratch, completion.output.output2)) {
+            results.add(toLookupResult(completion, spare));
             break;
           }
         }
@@ -611,9 +627,7 @@ public class AnalyzingSuggester extends Lookup {
             // In exactFirst mode, don't accept any paths
             // matching the surface form since that will
             // create duplicate results:
-            spare.grow(output.output2.length);
-            UnicodeUtil.UTF8toUTF16(output.output2, spare);
-            return CHARSEQUENCE_COMPARATOR.compare(spare, key) != 0;
+            return !isExactMatch(keyScratch, output.output2);
           }
         }
       };
@@ -625,9 +639,7 @@ public class AnalyzingSuggester extends Lookup {
       MinResult<Pair<Long,BytesRef>> completions[] = searcher.search();
 
       for(MinResult<Pair<Long,BytesRef>> completion : completions) {
-        spare.grow(completion.output.output2.length);
-        UnicodeUtil.UTF8toUTF16(completion.output.output2, spare);
-        LookupResult result = new LookupResult(spare.toString(), decodeWeight(completion.output.output1));
+        LookupResult result = toLookupResult(completion, spare);
         //System.out.println("    result=" + result);
         results.add(result);
       }
@@ -637,6 +649,20 @@ public class AnalyzingSuggester extends Lookup {
       throw new RuntimeException(bogus);
     }
   }
+  
+  protected boolean isExactMatch(BytesRef key, BytesRef surfaceForm) {
+    return key.bytesEquals(surfaceForm);
+  }
+  
+  /**
+   * Converts a completion result into a {@link LookupResult}. Custom implementation can override this
+   * method to apply custom conversions of surface forms into {@link LookupResult}.
+   */
+  protected LookupResult toLookupResult(MinResult<Pair<Long,BytesRef>> completion, CharsRef spare) {
+    spare.grow(completion.output.output2.length);
+    UnicodeUtil.UTF8toUTF16(completion.output.output2, spare);
+    return new LookupResult(spare.toString(), decodeWeight(completion.output.output1));
+  }
 
   /**
    * Returns the weight associated with an input string,
@@ -647,7 +673,7 @@ public class AnalyzingSuggester extends Lookup {
   }
   
   /** cost -> weight */
-  private static int decodeWeight(long encoded) {
+  protected static int decodeWeight(long encoded) {
     return (int)(Integer.MAX_VALUE - encoded);
   }
   
diff --git a/lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/SurfaceFormAttribute.java b/lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/SurfaceFormAttribute.java
new file mode 100644
index 0000000..d0fae6b
--- /dev/null
+++ b/lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/SurfaceFormAttribute.java
@@ -0,0 +1,39 @@
+package org.apache.lucene.search.suggest.analyzing;
+/*
+ * 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.
+ */
+
+import org.apache.lucene.util.Attribute;
+import org.apache.lucene.util.BytesRef;
+
+
+/**
+ * The surface form of a suggestion
+ */
+public interface SurfaceFormAttribute extends Attribute {
+
+  /**
+   * Returns the surface form of a suggestion if any. otherwise <code>null</code>
+   * @see SurfaceFormAttribute#setSurfaceForm(BytesRef)
+   */
+  public BytesRef getSurfaceForm();
+  
+  /**
+   * Sets this suggestions surface form.
+   * @see SurfaceFormAttribute#getSurfaceForm()
+   */
+  public void setSurfaceForm(BytesRef ref);
+}
diff --git a/lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/SurfaceFormAttributeImpl.java b/lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/SurfaceFormAttributeImpl.java
new file mode 100644
index 0000000..960351e
--- /dev/null
+++ b/lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/SurfaceFormAttributeImpl.java
@@ -0,0 +1,72 @@
+package org.apache.lucene.search.suggest.analyzing;
+/*
+ * 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.
+ */
+
+import org.apache.lucene.util.AttributeImpl;
+import org.apache.lucene.util.BytesRef;
+
+/**
+ * Default implemenation for {@link SurfaceFormAttribute}
+ */
+public class SurfaceFormAttributeImpl extends AttributeImpl implements
+    SurfaceFormAttribute {
+  
+  private BytesRef surfaceForm;
+  
+  @Override
+  public BytesRef getSurfaceForm() {
+    return surfaceForm;
+  }
+  
+  @Override
+  public void setSurfaceForm(BytesRef surfaceForm) {
+    this.surfaceForm = surfaceForm;
+  }
+  
+  @Override
+  public void clear() {
+    surfaceForm = null;
+  }
+  
+  @Override
+  public void copyTo(AttributeImpl target) {
+    SurfaceFormAttribute t = (SurfaceFormAttribute) target;
+    t.setSurfaceForm((surfaceForm == null) ? null : surfaceForm.clone());
+  }
+
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = 1;
+    result = prime * result
+        + ((surfaceForm == null) ? 0 : surfaceForm.hashCode());
+    return result;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) return true;
+    if (obj == null) return false;
+    if (getClass() != obj.getClass()) return false;
+    SurfaceFormAttributeImpl other = (SurfaceFormAttributeImpl) obj;
+    if (surfaceForm == null) {
+      if (other.surfaceForm != null) return false;
+    } else if (!surfaceForm.equals(other.surfaceForm)) return false;
+    return true;
+  }
+  
+}
diff --git a/lucene/suggest/src/test/org/apache/lucene/search/suggest/TermFreq.java b/lucene/suggest/src/test/org/apache/lucene/search/suggest/TermFreq.java
index 2b02ac1..8ef709a 100644
--- a/lucene/suggest/src/test/org/apache/lucene/search/suggest/TermFreq.java
+++ b/lucene/suggest/src/test/org/apache/lucene/search/suggest/TermFreq.java
@@ -22,13 +22,23 @@ import org.apache.lucene.util.BytesRef;
 public final class TermFreq {
   public final BytesRef term;
   public final long v;
+  public final BytesRef surfaceForm;
 
   public TermFreq(String term, long v) {
-   this(new BytesRef(term), v);
+   this(new BytesRef(term), v, new BytesRef(term));
   }
   
   public TermFreq(BytesRef term, long v) {
+    this(term, v, term);
+   }
+  
+  public TermFreq(String term, long v, String surfaceForm) {
+    this(new BytesRef(term), v, new BytesRef(surfaceForm));
+   }
+  
+  public TermFreq(BytesRef term, long v, BytesRef surfaceForm) {
     this.term = term;
     this.v = v;
+    this.surfaceForm = surfaceForm;
   }
 }
\ No newline at end of file
diff --git a/lucene/suggest/src/test/org/apache/lucene/search/suggest/TermFreqArrayIterator.java b/lucene/suggest/src/test/org/apache/lucene/search/suggest/TermFreqArrayIterator.java
index 06d7301..823168c 100644
--- a/lucene/suggest/src/test/org/apache/lucene/search/suggest/TermFreqArrayIterator.java
+++ b/lucene/suggest/src/test/org/apache/lucene/search/suggest/TermFreqArrayIterator.java
@@ -22,15 +22,18 @@ import java.util.Comparator;
 import java.util.Iterator;
 
 import org.apache.lucene.search.spell.TermFreqIterator;
+import org.apache.lucene.search.suggest.analyzing.SurfaceFormAttribute;
 import org.apache.lucene.util.BytesRef;
 
 /**
  * A {@link TermFreqIterator} over a sequence of {@link TermFreq}s.
  */
-public final class TermFreqArrayIterator implements TermFreqIterator {
+public final class TermFreqArrayIterator extends TermFreqIterator {
   private final Iterator<TermFreq> i;
   private TermFreq current;
   private final BytesRef spare = new BytesRef();
+  private final BytesRef surfaceSpare = new BytesRef();
+  private SurfaceFormAttribute surfaceForm = addAttribute(SurfaceFormAttribute.class);
 
   public TermFreqArrayIterator(Iterator<TermFreq> i) {
     this.i = i;
@@ -53,6 +56,7 @@ public final class TermFreqArrayIterator implements TermFreqIterator {
     if (i.hasNext()) {
       current = i.next();
       spare.copyBytes(current.term);
+      surfaceForm.setSurfaceForm(current.surfaceForm);
       return spare;
     }
     return null;
diff --git a/lucene/suggest/src/test/org/apache/lucene/search/suggest/analyzing/AnalyzingSuggesterTest.java b/lucene/suggest/src/test/org/apache/lucene/search/suggest/analyzing/AnalyzingSuggesterTest.java
index c883698..ee9968d 100644
--- a/lucene/suggest/src/test/org/apache/lucene/search/suggest/analyzing/AnalyzingSuggesterTest.java
+++ b/lucene/suggest/src/test/org/apache/lucene/search/suggest/analyzing/AnalyzingSuggesterTest.java
@@ -97,6 +97,47 @@ public class AnalyzingSuggesterTest extends LuceneTestCase {
     assertEquals(6, results.get(2).value, 0.01F);
   }
   
+  public void testDifferentSurfaceForm() throws IOException {
+    TermFreq keys[] = new TermFreq[] {
+        new TermFreq("foo", 50, "Some Foo Some Bar"),
+        new TermFreq("bar", 10, "Some Bar Some Foo"),
+        new TermFreq("barbar", 12),
+        new TermFreq("barbara", 6)
+    };
+    
+    AnalyzingSuggester suggester = new AnalyzingSuggester(new MockAnalyzer(random(), MockTokenizer.KEYWORD, false));
+    suggester.build(new TermFreqArrayIterator(keys));
+    
+    // top N of 2, but only foo is available
+    List<LookupResult> results = suggester.lookup(_TestUtil.stringToCharSequence("f", random()), false, 2);
+    assertEquals(1, results.size());
+    assertEquals("Some Foo Some Bar", results.get(0).key.toString());
+    assertEquals(50, results.get(0).value, 0.01F);
+    
+    results = suggester.lookup(_TestUtil.stringToCharSequence("bar", random()), false, 1);
+    assertEquals(1, results.size());
+    assertEquals("barbar", results.get(0).key.toString());
+    assertEquals(12, results.get(0).value, 0.01F);
+    
+    // top N Of 2 for 'b'
+    results = suggester.lookup(_TestUtil.stringToCharSequence("b", random()), false, 2);
+    assertEquals(2, results.size());
+    assertEquals("barbar", results.get(0).key.toString());
+    assertEquals(12, results.get(0).value, 0.01F);
+    assertEquals("Some Bar Some Foo", results.get(1).key.toString());
+    assertEquals(10, results.get(1).value, 0.01F);
+    
+    // top N of 3 for 'ba'
+    results = suggester.lookup(_TestUtil.stringToCharSequence("ba", random()), false, 3);
+    assertEquals(3, results.size());
+    assertEquals("barbar", results.get(0).key.toString());
+    assertEquals(12, results.get(0).value, 0.01F);
+    assertEquals("Some Bar Some Foo", results.get(1).key.toString());
+    assertEquals(10, results.get(1).value, 0.01F);
+    assertEquals("barbara", results.get(2).key.toString());
+    assertEquals(6, results.get(2).value, 0.01F);
+  }
+  
   // TODO: more tests
   /**
    * basic "standardanalyzer" test with stopword removal
diff --git a/lucene/suggest/src/test/org/apache/lucene/search/suggest/analyzing/CustomAnalyzingSuggesterTest.java b/lucene/suggest/src/test/org/apache/lucene/search/suggest/analyzing/CustomAnalyzingSuggesterTest.java
new file mode 100644
index 0000000..dd54f6a
--- /dev/null
+++ b/lucene/suggest/src/test/org/apache/lucene/search/suggest/analyzing/CustomAnalyzingSuggesterTest.java
@@ -0,0 +1,150 @@
+package org.apache.lucene.search.suggest.analyzing;
+/*
+ * 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.
+ */
+
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.MockAnalyzer;
+import org.apache.lucene.analysis.MockTokenizer;
+import org.apache.lucene.search.suggest.TermFreq;
+import org.apache.lucene.search.suggest.TermFreqArrayIterator;
+import org.apache.lucene.search.suggest.Lookup.LookupResult;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.CharsRef;
+import org.apache.lucene.util.LuceneTestCase;
+import org.apache.lucene.util.UnicodeUtil;
+import org.apache.lucene.util._TestUtil;
+import org.apache.lucene.util.fst.PairOutputs.Pair;
+import org.apache.lucene.util.fst.Util.MinResult;
+
+
+/**
+ * A testcase that test the extensibility of {@link AnalyzingSuggester} to encode 
+ * arbitrary surface forms
+ */
+public class CustomAnalyzingSuggesterTest extends LuceneTestCase {
+  
+  
+  public void testCustomSurfaceForm() throws IOException {
+    TermFreq keys[] = new TermFreq[] {
+        new TermFreq(new BytesRef("foo"), 50, encodeUserId("Foo", 1)),
+        new TermFreq(new BytesRef("bar"), 10, encodeUserId("Bar", 2)),
+        new TermFreq(new BytesRef("barbar"), 12, encodeUserId("Lucy Barbar", 3)),
+        new TermFreq(new BytesRef("barbara"), 6, encodeUserId("Barbara Streisand", 4))
+    };
+    
+    CustomAnalyzingSuggestor suggester = new CustomAnalyzingSuggestor(new MockAnalyzer(random(), MockTokenizer.KEYWORD, true));
+    suggester.build(new TermFreqArrayIterator(keys));
+    
+    // top N of 2, but only foo is available
+    List<LookupResult> results = suggester.lookup(_TestUtil.stringToCharSequence("f", random()), false, 2);
+    assertEquals(1, results.size());
+    assertEquals("Foo", results.get(0).key.toString());
+    assertEquals(50, results.get(0).value, 0.01F);
+    assertUserId(results.get(0), 1);
+    
+    results = suggester.lookup(_TestUtil.stringToCharSequence("Bar", random()), false, 1);
+    assertEquals(1, results.size());
+    assertEquals("Bar", results.get(0).key.toString());
+    assertEquals(10, results.get(0).value, 0.01F);
+    assertUserId(results.get(0), 2);
+    
+    // top N Of 2 for 'b'
+    results = suggester.lookup(_TestUtil.stringToCharSequence("b", random()), false, 2);
+    assertEquals(2, results.size());
+    assertEquals("Lucy Barbar", results.get(0).key.toString());
+    assertEquals(12, results.get(0).value, 0.01F);
+    assertUserId(results.get(0), 3);
+    assertEquals("Bar", results.get(1).key.toString());
+    assertEquals(10, results.get(1).value, 0.01F);
+    assertUserId(results.get(1), 2);
+    
+    // top N of 3 for 'ba'
+    results = suggester.lookup(_TestUtil.stringToCharSequence("ba", random()), false, 3);
+    assertEquals(3, results.size());
+    assertEquals("Lucy Barbar", results.get(0).key.toString());
+    assertEquals(12, results.get(0).value, 0.01F);
+    assertUserId(results.get(0), 3);
+    assertEquals("Bar", results.get(1).key.toString());
+    assertEquals(10, results.get(1).value, 0.01F);
+    assertUserId(results.get(1), 2);
+    assertEquals("Barbara Streisand", results.get(2).key.toString());
+    assertEquals(6, results.get(2).value, 0.01F);
+    assertUserId(results.get(2), 4);
+    
+  }
+  
+  private static void assertUserId(LookupResult res, int expectedId) {
+    assertTrue(res instanceof CustomLookupResult);
+    assertEquals(expectedId, ((CustomLookupResult)res).userId);
+  }
+  
+  private static BytesRef encodeUserId(String surfaceForm, int userId) {
+    BytesRef ref = new BytesRef(surfaceForm);
+    ref.grow(ref.length+4);
+    copyInt(ref, userId, ref.length);
+    ref.length +=4;
+    return ref;
+  }
+  
+  private static int parseInt(BytesRef b, int pos) {
+    return ((b.bytes[pos++] & 0xFF) << 24) | ((b.bytes[pos++] & 0xFF) << 16)
+        | ((b.bytes[pos++] & 0xFF) << 8) | (b.bytes[pos] & 0xFF);
+  }
+  
+  private static void copyInt(BytesRef ref, int value, int startOffset) {
+    ref.bytes[startOffset] = (byte) (value >> 24);
+    ref.bytes[startOffset + 1] = (byte) (value >> 16);
+    ref.bytes[startOffset + 2] = (byte) (value >> 8);
+    ref.bytes[startOffset + 3] = (byte) (value);
+  }
+
+  
+  public static class CustomLookupResult extends LookupResult {
+
+    int userId;
+
+    public CustomLookupResult(CharSequence key, long value, int userId) {
+      super(key, value);
+      this.userId = userId;
+    }
+  }
+  
+  public static class CustomAnalyzingSuggestor extends AnalyzingSuggester {
+
+    public CustomAnalyzingSuggestor(Analyzer analyzer) {
+      super(analyzer);
+    }
+
+    @Override
+    protected LookupResult toLookupResult(
+        MinResult<Pair<Long,BytesRef>> completion, CharsRef spare) {
+      spare.grow(completion.output.output2.length);
+      UnicodeUtil.UTF8toUTF16(completion.output.output2.bytes, completion.output.output2.offset, completion.output.output2.length-4, spare);
+      return new CustomLookupResult(spare.toString(), decodeWeight(completion.output.output1), parseInt(completion.output.output2, completion.output.output2.length-4));
+    }
+
+    @Override
+    protected boolean isExactMatch(BytesRef key, BytesRef surfaceForm) {
+      BytesRef withoutId = new BytesRef(surfaceForm.bytes, surfaceForm.offset, surfaceForm.length-4);
+      return key.bytesEquals(withoutId);
+    }
+    
+  }
+}
