/*
  * TS_execute callback for matching a tsquery operand to headline words
+ *
+ * Note: it's tempting to report words[] indexes as pos values to save
+ * searching in hlCover; but that would screw up phrase matching, which
+ * expects to measure distances in lexemes not tokens.
  */
 static TSTernaryValue
 checkcondition_HL(void *opaque, QueryOperand *val, ExecPhraseData *data)
    hlCheck    *checkval = (hlCheck *) opaque;
    int         i;
 
-   /* scan words array for marching items */
+   /* scan words array for matching items */
    for (i = 0; i < checkval->len; i++)
    {
        if (checkval->words[i].item == val)
    return TS_NO;
 }
 
-/*
- * hlFirstIndex: find first index >= pos containing any word used in query
- *
- * Returns -1 if no such index
- */
-static int
-hlFirstIndex(HeadlineParsedText *prs, int pos)
-{
-   int         i;
-
-   for (i = pos; i < prs->curwords; i++)
-   {
-       if (prs->words[i].item != NULL)
-           return i;
-   }
-   return -1;
-}
-
 /*
  * hlCover: try to find a substring of prs' word list that satisfies query
  *
- * At entry, *p must be the first word index to consider (initialize this
- * to zero, or to the next index after a previous successful search).
- * We will consider all substrings starting at or after that word, and
- * containing no more than max_cover words.  (We need a length limit to
- * keep this from taking O(N^2) time for a long document with many query
- * words but few complete matches.  Actually, since checkcondition_HL is
- * roughly O(N) in the length of the substring being checked, it's even
- * worse than that.)
+ * locations is the result of TS_execute_locations() for the query.
+ * We use this to identify plausible subranges of the query.
+ *
+ * *nextpos is the lexeme position (NOT word index) to start the search
+ * at.  Caller should initialize this to zero.  If successful, we'll
+ * advance it to the next place to search at.
  *
  * On success, sets *p to first word index and *q to last word index of the
  * cover substring, and returns true.
  * words used in the query.
  */
 static bool
-hlCover(HeadlineParsedText *prs, TSQuery query, int max_cover,
-       int *p, int *q)
+hlCover(HeadlineParsedText *prs, TSQuery query, List *locations,
+       int *nextpos, int *p, int *q)
 {
-   int         pmin,
-               pmax,
-               nextpmin,
-               nextpmax;
-   hlCheck     ch;
+   int         pos = *nextpos;
 
-   /*
-    * We look for the earliest, shortest substring of prs->words that
-    * satisfies the query.  Both the pmin and pmax indices must be words
-    * appearing in the query; there's no point in trying endpoints in between
-    * such points.
-    */
-   pmin = hlFirstIndex(prs, *p);
-   while (pmin >= 0)
+   /* This loop repeats when our selected word-range fails the query */
+   for (;;)
    {
-       /* This useless assignment just keeps stupider compilers quiet */
-       nextpmin = -1;
-       /* Consider substrings starting at pmin */
-       ch.words = &(prs->words[pmin]);
-       /* Consider the length-one substring first, then longer substrings */
-       pmax = pmin;
-       do
+       int         posb,
+                   pose;
+       ListCell   *lc;
+
+       /*
+        * For each AND'ed query term or phrase, find its first occurrence at
+        * or after pos; set pose to the maximum of those positions.
+        *
+        * We need not consider ORs or NOTs here; see the comments for
+        * TS_execute_locations().  Rechecking the match with TS_execute(),
+        * below, will deal with any ensuing imprecision.
+        */
+       pose = -1;
+       foreach(lc, locations)
+       {
+           ExecPhraseData *pdata = (ExecPhraseData *) lfirst(lc);
+           int         first = -1;
+
+           for (int i = 0; i < pdata->npos; i++)
+           {
+               /* For phrase matches, use the ending lexeme */
+               int         endp = pdata->pos[i];
+
+               if (endp >= pos)
+               {
+                   first = endp;
+                   break;
+               }
+           }
+           if (first < 0)
+               return false;   /* no more matches for this term */
+           if (first > pose)
+               pose = first;
+       }
+
+       if (pose < 0)
+           return false;       /* we only get here if empty list */
+
+       /*
+        * Now, for each AND'ed query term or phrase, find its last occurrence
+        * at or before pose; set posb to the minimum of those positions.
+        *
+        * We start posb at INT_MAX - 1 to guarantee no overflow if we compute
+        * posb + 1 below.
+        */
+       posb = INT_MAX - 1;
+       foreach(lc, locations)
        {
-           /* Try to match query against pmin .. pmax substring */
-           ch.len = pmax - pmin + 1;
-           if (TS_execute(GETQUERY(query), &ch,
-                          TS_EXEC_EMPTY, checkcondition_HL))
+           ExecPhraseData *pdata = (ExecPhraseData *) lfirst(lc);
+           int         last = -1;
+
+           for (int i = pdata->npos - 1; i >= 0; i--)
            {
-               *p = pmin;
-               *q = pmax;
-               return true;
+               /* For phrase matches, use the starting lexeme */
+               int         startp = pdata->pos[i] - pdata->width;
+
+               if (startp <= pose)
+               {
+                   last = startp;
+                   break;
+               }
            }
-           /* Nope, so advance pmax to next feasible endpoint */
-           nextpmax = hlFirstIndex(prs, pmax + 1);
+           if (last < posb)
+               posb = last;
+       }
+
+       /*
+        * We could end up with posb to the left of pos, in case some phrase
+        * match crosses pos.  Try the match starting at pos anyway, since the
+        * result of TS_execute_locations is imprecise for phrase matches OR'd
+        * with plain matches; that is, if the query is "(A <-> B) | C" then C
+        * could match at pos even though the phrase match would have to
+        * extend to the left of pos.
+        */
+       posb = Max(posb, pos);
 
+       /* This test probably always succeeds, but be paranoid */
+       if (posb <= pose)
+       {
            /*
-            * If this is our first advance past pmin, then the result is also
-            * the next feasible value of pmin; remember it to save a
-            * redundant search.
+            * posb .. pose is now the shortest, earliest-after-pos range of
+            * lexeme positions containing all the query terms.  It will
+            * contain all phrase matches, too, except in the corner case
+            * described just above.
+            *
+            * Now convert these lexeme positions to indexes in prs->words[].
             */
-           if (pmax == pmin)
-               nextpmin = nextpmax;
-           pmax = nextpmax;
+           int         idxb = -1;
+           int         idxe = -1;
+
+           for (int i = 0; i < prs->curwords; i++)
+           {
+               if (prs->words[i].item == NULL)
+                   continue;
+               if (idxb < 0 && prs->words[i].pos >= posb)
+                   idxb = i;
+               if (prs->words[i].pos <= pose)
+                   idxe = i;
+               else
+                   break;
+           }
+
+           /* This test probably always succeeds, but be paranoid */
+           if (idxb >= 0 && idxe >= idxb)
+           {
+               /*
+                * Finally, check that the selected range satisfies the query.
+                * This should succeed in all simple cases; but odd cases
+                * involving non-top-level NOT conditions or phrase matches
+                * OR'd with other things could fail, since the result of
+                * TS_execute_locations doesn't fully represent such things.
+                */
+               hlCheck     ch;
+
+               ch.words = &(prs->words[idxb]);
+               ch.len = idxe - idxb + 1;
+               if (TS_execute(GETQUERY(query), &ch,
+                              TS_EXEC_EMPTY, checkcondition_HL))
+               {
+                   /* Match!  Advance *nextpos and return the word range. */
+                   *nextpos = posb + 1;
+                   *p = idxb;
+                   *q = idxe;
+                   return true;
+               }
+           }
        }
-       while (pmax >= 0 && pmax - pmin < max_cover);
-       /* No luck here, so try next feasible startpoint */
-       pmin = nextpmin;
+
+       /*
+        * Advance pos and try again.  Any later workable match must start
+        * beyond posb.
+        */
+       pos = posb + 1;
    }
+   /* Can't get here, but stupider compilers complain if we leave it off */
    return false;
 }
 
  * it only controls presentation details.
  */
 static void
-mark_hl_fragments(HeadlineParsedText *prs, TSQuery query, bool highlightall,
+mark_hl_fragments(HeadlineParsedText *prs, TSQuery query, List *locations,
+                 bool highlightall,
                  int shortword, int min_words,
-                 int max_words, int max_fragments, int max_cover)
+                 int max_words, int max_fragments)
 {
    int32       poslen,
                curlen,
 
    int32       startpos = 0,
                endpos = 0,
+               nextpos = 0,
                p = 0,
                q = 0;
 
    covers = palloc(maxcovers * sizeof(CoverPos));
 
    /* get all covers */
-   while (hlCover(prs, query, max_cover, &p, &q))
+   while (hlCover(prs, query, locations, &nextpos, &p, &q))
    {
        startpos = p;
        endpos = q;
            startpos = endpos + 1;
            endpos = q;
        }
-
-       /* move p to generate the next cover */
-       p++;
    }
 
    /* choose best covers */
  * Headline selector used when MaxFragments == 0
  */
 static void
-mark_hl_words(HeadlineParsedText *prs, TSQuery query, bool highlightall,
-             int shortword, int min_words, int max_words, int max_cover)
+mark_hl_words(HeadlineParsedText *prs, TSQuery query, List *locations,
+             bool highlightall,
+             int shortword, int min_words, int max_words)
 {
-   int         p = 0,
+   int         nextpos = 0,
+               p = 0,
                q = 0;
    int         bestb = -1,
                beste = -1;
    if (!highlightall)
    {
        /* examine all covers, select a headline using the best one */
-       while (hlCover(prs, query, max_cover, &p, &q))
+       while (hlCover(prs, query, locations, &nextpos, &p, &q))
        {
            /*
             * Count words (curlen) and interesting words (poslen) within
                bestlen = poslen;
                bestcover = poscover;
            }
-
-           /* move p to generate the next cover */
-           p++;
        }
 
        /*
    HeadlineParsedText *prs = (HeadlineParsedText *) PG_GETARG_POINTER(0);
    List       *prsoptions = (List *) PG_GETARG_POINTER(1);
    TSQuery     query = PG_GETARG_TSQUERY(2);
+   hlCheck     ch;
+   List       *locations;
 
    /* default option values: */
    int         min_words = 15;
    int         shortword = 3;
    int         max_fragments = 0;
    bool        highlightall = false;
-   int         max_cover;
    ListCell   *l;
 
    /* Extract configuration option values */
                            defel->defname)));
    }
 
-   /*
-    * We might eventually make max_cover a user-settable parameter, but for
-    * now, just compute a reasonable value based on max_words and
-    * max_fragments.
-    */
-   max_cover = Max(max_words * 10, 100);
-   if (max_fragments > 0)
-       max_cover *= max_fragments;
-
    /* in HighlightAll mode these parameters are ignored */
    if (!highlightall)
    {
                     errmsg("MaxFragments should be >= 0")));
    }
 
+   /* Locate words and phrases matching the query */
+   ch.words = prs->words;
+   ch.len = prs->curwords;
+   locations = TS_execute_locations(GETQUERY(query), &ch, TS_EXEC_EMPTY,
+                                    checkcondition_HL);
+
    /* Apply appropriate headline selector */
    if (max_fragments == 0)
-       mark_hl_words(prs, query, highlightall, shortword,
-                     min_words, max_words, max_cover);
+       mark_hl_words(prs, query, locations, highlightall, shortword,
+                     min_words, max_words);
    else
-       mark_hl_fragments(prs, query, highlightall, shortword,
-                         min_words, max_words, max_fragments, max_cover);
+       mark_hl_fragments(prs, query, locations, highlightall, shortword,
+                         min_words, max_words, max_fragments);
 
    /* Fill in default values for string options */
    if (!prs->startsel)
 
 static TSTernaryValue TS_execute_recurse(QueryItem *curitem, void *arg,
                                         uint32 flags,
                                         TSExecuteCallback chkcond);
+static bool TS_execute_locations_recurse(QueryItem *curitem,
+                                        void *arg,
+                                        TSExecuteCallback chkcond,
+                                        List **locations);
 static int tsvector_bsearch(const TSVector tsv, char *lexeme, int lexeme_len);
 static Datum tsvector_update_trigger(PG_FUNCTION_ARGS, bool config_column);
 
  * In addition to the same arguments used for TS_execute, the caller may pass
  * a preinitialized-to-zeroes ExecPhraseData struct, to be filled with lexeme
  * match position info on success.  data == NULL if no position data need be
- * returned.  (In practice, outside callers pass NULL, and only the internal
- * recursion cases pass a data pointer.)
+ * returned.
  * Note: the function assumes data != NULL for operators other than OP_PHRASE.
- * This is OK because an outside call always starts from an OP_PHRASE node.
+ * This is OK because an outside call always starts from an OP_PHRASE node,
+ * and all internal recursion cases pass data != NULL.
  *
  * The detailed semantics of the match data, given that the function returned
  * TS_YES (successful match), are:
    return TS_NO;
 }
 
+/*
+ * Evaluate tsquery and report locations of matching terms.
+ *
+ * This is like TS_execute except that it returns match locations not just
+ * success/failure status.  The callback function is required to provide
+ * position data (we report failure if it doesn't).
+ *
+ * On successful match, the result is a List of ExecPhraseData structs, one
+ * for each AND'ed term or phrase operator in the query.  Each struct includes
+ * a sorted array of lexeme positions matching that term.  (Recall that for
+ * phrase operators, the match includes width+1 lexemes, and the recorded
+ * position is that of the rightmost lexeme.)
+ *
+ * OR subexpressions are handled by union'ing their match locations into a
+ * single List element, which is valid since any of those locations contains
+ * a match.  However, when some of the OR'ed terms are phrase operators, we
+ * report the maximum width of any of the OR'ed terms, making such cases
+ * slightly imprecise in the conservative direction.  (For example, if the
+ * tsquery is "(A <-> B) | C", an occurrence of C in the data would be
+ * reported as though it includes the lexeme to the left of C.)
+ *
+ * Locations of NOT subexpressions are not reported.  (Obviously, there can
+ * be no successful NOT matches at top level, or the match would have failed.
+ * So this amounts to ignoring NOTs underneath ORs.)
+ *
+ * The result is NIL if no match, or if position data was not returned.
+ *
+ * Arguments are the same as for TS_execute, although flags is currently
+ * vestigial since none of the defined bits are sensible here.
+ */
+List *
+TS_execute_locations(QueryItem *curitem, void *arg,
+                    uint32 flags,
+                    TSExecuteCallback chkcond)
+{
+   List       *result;
+
+   /* No flags supported, as yet */
+   Assert(flags == TS_EXEC_EMPTY);
+   if (TS_execute_locations_recurse(curitem, arg, chkcond, &result))
+       return result;
+   return NIL;
+}
+
+/*
+ * TS_execute_locations recursion for operators above any phrase operator.
+ * OP_PHRASE subexpressions can be passed off to TS_phrase_execute.
+ */
+static bool
+TS_execute_locations_recurse(QueryItem *curitem, void *arg,
+                            TSExecuteCallback chkcond,
+                            List **locations)
+{
+   bool        lmatch,
+               rmatch;
+   List       *llocations,
+              *rlocations;
+   ExecPhraseData *data;
+
+   /* since this function recurses, it could be driven to stack overflow */
+   check_stack_depth();
+
+   /* ... and let's check for query cancel while we're at it */
+   CHECK_FOR_INTERRUPTS();
+
+   /* Default locations result is empty */
+   *locations = NIL;
+
+   if (curitem->type == QI_VAL)
+   {
+       data = palloc0_object(ExecPhraseData);
+       if (chkcond(arg, (QueryOperand *) curitem, data) == TS_YES)
+       {
+           *locations = list_make1(data);
+           return true;
+       }
+       pfree(data);
+       return false;
+   }
+
+   switch (curitem->qoperator.oper)
+   {
+       case OP_NOT:
+           if (!TS_execute_locations_recurse(curitem + 1, arg, chkcond,
+                                             &llocations))
+               return true;    /* we don't pass back any locations */
+           return false;
+
+       case OP_AND:
+           if (!TS_execute_locations_recurse(curitem + curitem->qoperator.left,
+                                             arg, chkcond,
+                                             &llocations))
+               return false;
+           if (!TS_execute_locations_recurse(curitem + 1,
+                                             arg, chkcond,
+                                             &rlocations))
+               return false;
+           *locations = list_concat(llocations, rlocations);
+           return true;
+
+       case OP_OR:
+           lmatch = TS_execute_locations_recurse(curitem + curitem->qoperator.left,
+                                                 arg, chkcond,
+                                                 &llocations);
+           rmatch = TS_execute_locations_recurse(curitem + 1,
+                                                 arg, chkcond,
+                                                 &rlocations);
+           if (lmatch || rmatch)
+           {
+               /*
+                * We generate an AND'able location struct from each
+                * combination of sub-matches, following the disjunctive law
+                * (A & B) | (C & D) = (A | C) & (A | D) & (B | C) & (B | D).
+                *
+                * However, if either input didn't produce locations (i.e., it
+                * failed or was a NOT), we must just return the other list.
+                */
+               if (llocations == NIL)
+                   *locations = rlocations;
+               else if (rlocations == NIL)
+                   *locations = llocations;
+               else
+               {
+                   ListCell   *ll;
+
+                   foreach(ll, llocations)
+                   {
+                       ExecPhraseData *ldata = (ExecPhraseData *) lfirst(ll);
+                       ListCell   *lr;
+
+                       foreach(lr, rlocations)
+                       {
+                           ExecPhraseData *rdata = (ExecPhraseData *) lfirst(lr);
+
+                           data = palloc0_object(ExecPhraseData);
+                           (void) TS_phrase_output(data, ldata, rdata,
+                                                   TSPO_BOTH | TSPO_L_ONLY | TSPO_R_ONLY,
+                                                   0, 0,
+                                                   ldata->npos + rdata->npos);
+                           /* Report the larger width, as explained above. */
+                           data->width = Max(ldata->width, rdata->width);
+                           *locations = lappend(*locations, data);
+                       }
+                   }
+               }
+
+               return true;
+           }
+           return false;
+
+       case OP_PHRASE:
+           /* We can hand this off to TS_phrase_execute */
+           data = palloc0_object(ExecPhraseData);
+           if (TS_phrase_execute(curitem, arg, TS_EXEC_EMPTY, chkcond,
+                                 data) == TS_YES)
+           {
+               if (!data->negate)
+                   *locations = list_make1(data);
+               return true;
+           }
+           pfree(data);
+           return false;
+
+       default:
+           elog(ERROR, "unrecognized operator: %d", curitem->qoperator.oper);
+   }
+
+   /* not reachable, but keep compiler quiet */
+   return false;
+}
+
 /*
  * Detect whether a tsquery boolean expression requires any positive matches
  * to values shown in the tsquery.
 
 extern TSTernaryValue TS_execute_ternary(QueryItem *curitem, void *arg,
                                         uint32 flags,
                                         TSExecuteCallback chkcond);
+extern List *TS_execute_locations(QueryItem *curitem, void *arg,
+                                 uint32 flags,
+                                 TSExecuteCallback chkcond);
 extern bool tsquery_requires_match(QueryItem *curitem);
 
 /*
 
 Water, water, every where,
   Nor any drop to drink.
 S. T. Coleridge (1772-1834)
-', phraseto_tsquery('english', 'painted Ocean'));
-              ts_headline              
----------------------------------------
- <b>painted</b> Ship                  +
-   Upon a <b>painted</b> <b>Ocean</b>.+
- Water, water, every where            +
+', to_tsquery('english', 'day & drink'));
+            ts_headline             
+------------------------------------
+ <b>day</b>,                       +
+   We stuck, nor breath nor motion,+
+ As idle as a painted Ship         +
+   Upon a painted Ocean.           +
+ Water, water, every where         +
+   And all the boards did shrink;  +
+ Water, water, every where,        +
+   Nor any drop
+(1 row)
+
+SELECT ts_headline('english', '
+Day after day, day after day,
+  We stuck, nor breath nor motion,
+As idle as a painted Ship
+  Upon a painted Ocean.
+Water, water, every where
+  And all the boards did shrink;
+Water, water, every where,
+  Nor any drop to drink.
+S. T. Coleridge (1772-1834)
+', to_tsquery('english', 'day | drink'));
+                        ts_headline                        
+-----------------------------------------------------------
+ <b>Day</b> after <b>day</b>, <b>day</b> after <b>day</b>,+
+   We stuck, nor breath nor motion,                       +
+ As idle as a painted
+(1 row)
+
+SELECT ts_headline('english', '
+Day after day, day after day,
+  We stuck, nor breath nor motion,
+As idle as a painted Ship
+  Upon a painted Ocean.
+Water, water, every where
+  And all the boards did shrink;
+Water, water, every where,
+  Nor any drop to drink.
+S. T. Coleridge (1772-1834)
+', to_tsquery('english', 'day | !drink'));
+                        ts_headline                        
+-----------------------------------------------------------
+ <b>Day</b> after <b>day</b>, <b>day</b> after <b>day</b>,+
+   We stuck, nor breath nor motion,                       +
+ As idle as a painted
+(1 row)
+
+SELECT ts_headline('english', '
+Day after day, day after day,
+  We stuck, nor breath nor motion,
+As idle as a painted Ship
+  Upon a painted Ocean.
+Water, water, every where
+  And all the boards did shrink;
+Water, water, every where,
+  Nor any drop to drink.
+S. T. Coleridge (1772-1834)
+', to_tsquery('english', 'painted <-> Ship & drink'));
+           ts_headline            
+----------------------------------
+ <b>painted</b> <b>Ship</b>      +
+   Upon a <b>painted</b> Ocean.  +
+ Water, water, every where       +
+   And all the boards did shrink;+
+ Water, water, every where,      +
+   Nor any drop to <b>drink</b>
+(1 row)
+
+SELECT ts_headline('english', '
+Day after day, day after day,
+  We stuck, nor breath nor motion,
+As idle as a painted Ship
+  Upon a painted Ocean.
+Water, water, every where
+  And all the boards did shrink;
+Water, water, every where,
+  Nor any drop to drink.
+S. T. Coleridge (1772-1834)
+', to_tsquery('english', 'painted <-> Ship | drink'));
+           ts_headline           
+---------------------------------
+ <b>painted</b> <b>Ship</b>     +
+   Upon a <b>painted</b> Ocean. +
+ Water, water, every where      +
+   And all the boards did shrink
+(1 row)
+
+SELECT ts_headline('english', '
+Day after day, day after day,
+  We stuck, nor breath nor motion,
+As idle as a painted Ship
+  Upon a painted Ocean.
+Water, water, every where
+  And all the boards did shrink;
+Water, water, every where,
+  Nor any drop to drink.
+S. T. Coleridge (1772-1834)
+', to_tsquery('english', 'painted <-> Ship | !drink'));
+           ts_headline           
+---------------------------------
+ <b>painted</b> <b>Ship</b>     +
+   Upon a <b>painted</b> Ocean. +
+ Water, water, every where      +
    And all the boards did shrink
 (1 row)
 
+SELECT ts_headline('english', '
+Day after day, day after day,
+  We stuck, nor breath nor motion,
+As idle as a painted Ship
+  Upon a painted Ocean.
+Water, water, every where
+  And all the boards did shrink;
+Water, water, every where,
+  Nor any drop to drink.
+S. T. Coleridge (1772-1834)
+', phraseto_tsquery('english', 'painted Ocean'));
+           ts_headline            
+----------------------------------
+ <b>painted</b> <b>Ocean</b>.    +
+ Water, water, every where       +
+   And all the boards did shrink;+
+ Water, water, every
+(1 row)
+
 SELECT ts_headline('english', '
 Day after day, day after day,
   We stuck, nor breath nor motion,
  <b>Lorem</b> ipsum <b>urna</b>.  Nullam nullam <b>ullamcorper</b> <b>urna</b>
 (1 row)
 
+SELECT ts_headline('english',
+'Lorem ipsum urna.  Nullam nullam ullamcorper urna.',
+phraseto_tsquery('english','ullamcorper urna'),
+'MaxWords=100, MinWords=5');
+                         ts_headline                         
+-------------------------------------------------------------
+ <b>urna</b>.  Nullam nullam <b>ullamcorper</b> <b>urna</b>.
+(1 row)
+
 SELECT ts_headline('english', '
 <html>
 <!-- some comment -->
 (1 row)
 
 SELECT ts_headline('simple', '1 2 3 1 3'::text, '1 <-> 3', 'MaxWords=4, MinWords=1');
-        ts_headline         
-----------------------------
- <b>3</b> <b>1</b> <b>3</b>
+    ts_headline    
+-------------------
+ <b>1</b> <b>3</b>
 (1 row)
 
 --Check if headline fragments work
 
 S. T. Coleridge (1772-1834)
 ', to_tsquery('english', 'ocean'));
 
+SELECT ts_headline('english', '
+Day after day, day after day,
+  We stuck, nor breath nor motion,
+As idle as a painted Ship
+  Upon a painted Ocean.
+Water, water, every where
+  And all the boards did shrink;
+Water, water, every where,
+  Nor any drop to drink.
+S. T. Coleridge (1772-1834)
+', to_tsquery('english', 'day & drink'));
+
+SELECT ts_headline('english', '
+Day after day, day after day,
+  We stuck, nor breath nor motion,
+As idle as a painted Ship
+  Upon a painted Ocean.
+Water, water, every where
+  And all the boards did shrink;
+Water, water, every where,
+  Nor any drop to drink.
+S. T. Coleridge (1772-1834)
+', to_tsquery('english', 'day | drink'));
+
+SELECT ts_headline('english', '
+Day after day, day after day,
+  We stuck, nor breath nor motion,
+As idle as a painted Ship
+  Upon a painted Ocean.
+Water, water, every where
+  And all the boards did shrink;
+Water, water, every where,
+  Nor any drop to drink.
+S. T. Coleridge (1772-1834)
+', to_tsquery('english', 'day | !drink'));
+
+SELECT ts_headline('english', '
+Day after day, day after day,
+  We stuck, nor breath nor motion,
+As idle as a painted Ship
+  Upon a painted Ocean.
+Water, water, every where
+  And all the boards did shrink;
+Water, water, every where,
+  Nor any drop to drink.
+S. T. Coleridge (1772-1834)
+', to_tsquery('english', 'painted <-> Ship & drink'));
+
+SELECT ts_headline('english', '
+Day after day, day after day,
+  We stuck, nor breath nor motion,
+As idle as a painted Ship
+  Upon a painted Ocean.
+Water, water, every where
+  And all the boards did shrink;
+Water, water, every where,
+  Nor any drop to drink.
+S. T. Coleridge (1772-1834)
+', to_tsquery('english', 'painted <-> Ship | drink'));
+
+SELECT ts_headline('english', '
+Day after day, day after day,
+  We stuck, nor breath nor motion,
+As idle as a painted Ship
+  Upon a painted Ocean.
+Water, water, every where
+  And all the boards did shrink;
+Water, water, every where,
+  Nor any drop to drink.
+S. T. Coleridge (1772-1834)
+', to_tsquery('english', 'painted <-> Ship | !drink'));
+
 SELECT ts_headline('english', '
 Day after day, day after day,
   We stuck, nor breath nor motion,
 to_tsquery('english','Lorem') && phraseto_tsquery('english','ullamcorper urna'),
 'MaxWords=100, MinWords=1');
 
+SELECT ts_headline('english',
+'Lorem ipsum urna.  Nullam nullam ullamcorper urna.',
+phraseto_tsquery('english','ullamcorper urna'),
+'MaxWords=100, MinWords=5');
+
 SELECT ts_headline('english', '
 <html>
 <!-- some comment -->