diff --git a/include/ajax.orgs.php b/include/ajax.orgs.php
index c4d73187bf6d2c85c2bf1dc4a7c0f6650174e233..0cc098a50ea44a9bb3863b243c2617690cdbdee4 100644
--- a/include/ajax.orgs.php
+++ b/include/ajax.orgs.php
@@ -26,26 +26,32 @@ class OrgsAjaxAPI extends AjaxController {
             Http::response(400, 'Query argument is required');
         }
 
+        $q = $_REQUEST['q'];
         $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
-        $orgs=array();
-
-        $escaped = db_input(strtolower($_REQUEST['q']), false);
-        $sql='SELECT DISTINCT org.id, org.name '
-            .' FROM '.ORGANIZATION_TABLE.' org '
-            .' LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON (entry.object_type=\'O\' AND entry.object_id = org.id)
-               LEFT JOIN '.FORM_ANSWER_TABLE.' value ON (value.entry_id=entry.id) '
-            .' WHERE org.name LIKE \'%'.$escaped.'%\' OR value.value LIKE \'%'.$escaped.'%\''
-            .' ORDER BY org.created '
-            .' LIMIT '.$limit;
-
-        if(($res=db_query($sql)) && db_num_rows($res)){
-            while(list($id, $name)=db_fetch_row($res)) {
-                $orgs[] = array('name' => Format::htmlchars($name), 'info' => $name,
-                    'id' => $id, '/bin/true' => $_REQUEST['q']);
-            }
+
+        $orgs = Organization::objects()
+            ->values_flat('id', 'name')
+            ->limit($limit);
+
+        global $ost;
+        $orgs = $ost->searcher->find($q, $orgs);
+        $orgs->order_by(new SqlCode('__relevance__'), QuerySet::DESC)
+            ->distinct('id');
+
+        if (!count($orgs) && substr($q, strlen($q)-1) != '*') {
+            // Do wildcard full-text search
+            $_REQUEST['q'] = $q."*";
+            return $this->search($type);
+        }
+
+        $matched = array();
+        foreach ($orgs as $O) {
+            list($id, $name) = $O;
+            $matched[] = array('name' => Format::htmlchars($name), 'info' => $name,
+                'id' => $id, '/bin/true' => $_REQUEST['q']);
         }
 
-        return $this->json_encode(array_values($orgs));
+        return $this->json_encode(array_values($matched));
 
     }
 
diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index 36e0f6aa8dd209427c2d896621be6985c05ffdfc..88410826294050e92b8ef09c1f4648e231eb6a94 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -79,17 +79,25 @@ class TicketsAjaxAPI extends AjaxController {
         }
 
         $hits = TicketModel::objects()
-            ->filter(Q::any(array(
-                'user__emails__address__contains' => $_REQUEST['q'],
-                'user__name__contains' => $_REQUEST['q'],
-                'user__account__username' => $_REQUEST['q'],
-                'user__org__name__contains' => $_REQUEST['q'],
-            )))
             ->filter($visibility)
             ->values('user__emails__address')
             ->annotate(array('tickets' => SqlAggregate::COUNT('ticket_id')))
             ->limit($limit);
 
+        $q = $_REQUEST['q'];
+        // Drop at sign in email addresses
+        $q = str_replace('@', ' ', $q);
+
+        global $ost;
+        $hits = $ost->searcher->find($q, $hits)
+            ->order_by(new SqlCode('__relevance__'), QuerySet::DESC);
+
+        if (!count($hits) && $q[strlen($q)-1] != '*') {
+            // Do wild-card fulltext search
+            $_REQUEST['q'] = $q.'*';
+            return $this->lookupByEmail();
+        }
+
         foreach ($hits as $T) {
             $email = $T['user__emails__address'];
             $count = $T['tickets'];
diff --git a/include/ajax.users.php b/include/ajax.users.php
index 6cfed17c8c09ddc6d2c0ccffe7bc9f5822e7bf53..0dd6da74b3384f14d16800c6c36ca657220cf7d6 100644
--- a/include/ajax.users.php
+++ b/include/ajax.users.php
@@ -29,54 +29,61 @@ class UsersAjaxAPI extends AjaxController {
             Http::response(400, __('Query argument is required'));
         }
 
+        $q = $_REQUEST['q'];
         $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
         $users=array();
         $emails=array();
 
         if (!$type || !strcasecmp($type, 'remote')) {
-            foreach (AuthenticationBackend::searchUsers($_REQUEST['q']) as $u) {
+            foreach (AuthenticationBackend::searchUsers($q) as $u) {
                 $name = new UsersName(array('first' => $u['first'], 'last' => $u['last']));
                 $users[] = array('email' => $u['email'], 'name'=>(string) $name,
                     'info' => "{$u['email']} - $name (remote)",
-                    'id' => "auth:".$u['id'], "/bin/true" => $_REQUEST['q']);
+                    'id' => "auth:".$u['id'], "/bin/true" => $q);
                 $emails[] = $u['email'];
             }
         }
 
         if (!$type || !strcasecmp($type, 'local')) {
-            $remote_emails = ($emails = array_filter($emails))
-                ? ' OR email.address IN ('.implode(',',db_input($emails)).') '
-                : '';
-
-            $q = str_replace(' ', '%', $_REQUEST['q']);
-            $escaped = db_input($q, false);
-            $sql='SELECT DISTINCT user.id, email.address, name '
-                .' FROM '.USER_TABLE.' user '
-                .' JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id '
-                .' LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON (entry.object_type=\'U\' AND entry.object_id = user.id)
-                   LEFT JOIN '.FORM_ANSWER_TABLE.' value ON (value.entry_id=entry.id) '
-                .' WHERE email.address LIKE \'%'.$escaped.'%\'
-                   OR user.name LIKE \'%'.$escaped.'%\'
-                   OR value.value LIKE \'%'.$escaped.'%\''.$remote_emails
-                .' LIMIT '.$limit;
-
-            if(($res=db_query($sql)) && db_num_rows($res)){
-                while(list($id,$email,$name)=db_fetch_row($res)) {
-                    foreach ($users as $i=>$u) {
-                        if ($u['email'] == $email) {
-                            unset($users[$i]);
-                            break;
-                        }
+
+            $users = User::objects()
+                ->values_flat('id', 'emails__address', 'name')
+                ->limit($limit);
+
+            global $ost;
+            $users = $ost->searcher->find($q, $users);
+            $users->order_by(new SqlCode('__relevance__'), QuerySet::DESC)
+                ->distinct('id');
+
+            if (!count($emails) && !count($users) && substr($q, strlen($q)-1) != '*') {
+                // Do wildcard full-text search
+                $_REQUEST['q'] = $q."*";
+                return $this->search($type);
+            }
+
+            if ($emails = array_filter($emails)) {
+                $users->chain(User::objects()->filter(array(
+                    'emails__address__in' => $emails
+                )));
+            }
+
+            $matches = array();
+            foreach ($users as $U) {
+                list($id,$email,$name) = $U;
+                foreach ($matches as $i=>$u) {
+                    if ($u['email'] == $email) {
+                        unset($matches[$i]);
+                        break;
                     }
-                    $name = Format::htmlchars(new UsersName($name));
-                    $users[] = array('email'=>$email, 'name'=>$name, 'info'=>"$email - $name",
-                        "id" => $id, "/bin/true" => $_REQUEST['q']);
                 }
+                $name = Format::htmlchars(new UsersName($name));
+                $matches[] = array('email'=>$email, 'name'=>$name, 'info'=>"$email - $name",
+                    "id" => $id, "/bin/true" => $_REQUEST['q']);
             }
-            usort($users, function($a, $b) { return strcmp($a['name'], $b['name']); });
+            usort($matches, function($a, $b) { return strcmp($a['name'], $b['name']); });
         }
 
-        return $this->json_encode(array_values($users));
+        return $this->json_encode(array_values($matches));
 
     }
 
diff --git a/include/class.search.php b/include/class.search.php
index a9ca110035d90bbd8b5153e26ba85f1ed202e3d3..4cf233fab8581c7a2c0e656b988750d79d460167 100644
--- a/include/class.search.php
+++ b/include/class.search.php
@@ -333,11 +333,11 @@ class MysqlSearchBackend extends SearchBackend {
         // If using boolean operators, search in boolean mode. This regex
         // will ensure proper placement of operators, whitespace, and quotes
         // in an effort to avoid crashing the query at MySQL
-        if (preg_match('/^(?:[(+<>~-]*(\w+[*]?|"[^"]+")[)]?(\s+|$))+$/u', $query, $T = array()))
+        $query = $this->quote($query);
+        if (preg_match('/(?=(?:^|\s)[()"+<>~-])\s*(?:[(+<>~-]*(\w+[*]?|"[^"]+")[)]?(\s+|$))+$|\w[*]/u', $query, $T = array()))
             $mode = ' IN BOOLEAN MODE';
         #elseif (count(explode(' ', $query)) == 1)
         #    $mode = ' WITH QUERY EXPANSION';
-        $query = $this->quote($query);
         $search = 'MATCH (Z1.title, Z1.content) AGAINST ('.db_input($query).$mode.')';
 
         switch ($criteria->model) {
@@ -349,10 +349,37 @@ class MysqlSearchBackend extends SearchBackend {
                 ),
                 'tables' => array(
                     str_replace(array(':', '{}'), array(TABLE_PREFIX, $search),
-                        "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`) as `ticket_id`, {} AS `relevance` FROM `:_search` Z1 LEFT JOIN `:thread_entry` Z2 ON (Z1.`object_type` = 'H' AND Z1.`object_id` = Z2.`id`) LEFT JOIN `:thread` Z3 ON (Z2.`thread_id` = Z3.`id` AND Z3.`object_type` = 'T') LEFT JOIN `:ticket` Z5 ON (Z1.`object_type` = 'T' AND Z1.`object_id` = Z5.`ticket_id`) WHERE {}) Z1"),
+                    "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`, Z8.`ticket_id`) as `ticket_id`, {} AS `relevance` FROM `:_search` Z1 LEFT JOIN `:thread_entry` Z2 ON (Z1.`object_type` = 'H' AND Z1.`object_id` = Z2.`id`) LEFT JOIN `:thread` Z3 ON (Z2.`thread_id` = Z3.`id` AND Z3.`object_type` = 'T') LEFT JOIN `:ticket` Z5 ON (Z1.`object_type` = 'T' AND Z1.`object_id` = Z5.`ticket_id`) LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') LEFT JOIN :ticket Z8 ON (Z8.`user_id` = Z6.`id`) WHERE {}) Z1"),
+                )
+            ));
+            $criteria->filter(array('ticket_id'=>new SqlCode('Z1.`ticket_id`')));
+            break;
+
+        case 'User':
+            $criteria->extra(array(
+                'select' => array(
+                    '__relevance__' => 'Z1.`relevance`',
+                ),
+                'tables' => array(
+                    str_replace(array(':', '{}'), array(TABLE_PREFIX, $search),
+                    "(SELECT Z6.`id` as `user_id`, {} AS `relevance` FROM `:_search` Z1 LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') WHERE {}) Z1"),
                 )
             ));
-            $criteria->filter(array('ticket_id'=>new SqlCode('Z1.`ticket_id`')))->distinct('ticket_id');
+            $criteria->filter(array('id'=>new SqlCode('Z1.`user_id`')));
+            break;
+
+        case 'Organization':
+            $criteria->extra(array(
+                'select' => array(
+                    '__relevance__' => 'Z1.`relevance`',
+                ),
+                'tables' => array(
+                    str_replace(array(':', '{}'), array(TABLE_PREFIX, $search),
+                    "(SELECT Z2.`id` as `org_id`, {} AS `relevance` FROM `:_search` Z1 LEFT JOIN `:organization` Z2 ON (Z2.`id` = Z1.`object_id` AND Z1.`object_type` = 'O') WHERE {}) Z1"),
+                )
+            ));
+            $criteria->filter(array('id'=>new SqlCode('Z1.`org_id`')));
+            break;
         }
 
         // TODO: Ensure search table exists;
diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php
index 2de6870684e91b2dab0be14d1bb03182c3f9d643..cf19f45a36db6dcbee8303203c95e84109e2107c 100644
--- a/include/staff/tickets.inc.php
+++ b/include/staff/tickets.inc.php
@@ -81,31 +81,30 @@ case 'search':
     if ($_REQUEST['query']) {
         $results_type=__('Search Results');
         // Use an index if possible
-        if ($_REQUEST['search-type'] == 'typeahead' && Validator::is_email($_REQUEST['query'])) {
-            $tickets = $tickets->filter(array(
-                'user__emails__address' => $_REQUEST['query'],
-            ));
+        if ($_REQUEST['search-type'] == 'typeahead') {
+            if (Validator::is_email($_REQUEST['query'])) {
+                $tickets = $tickets->filter(array(
+                    'user__emails__address' => $_REQUEST['query'],
+                ));
+            }
+            elseif (is_numeric($_REQUEST['query'])) {
+                $tickets = $tickets->filter(array(
+                    'number' => $_REQUEST['query'],
+                ));
+            }
         }
-        else {
-            $basic_search = Q::any(array(
-                'number__startswith' => $_REQUEST['query'],
-                'user__name__contains' => $_REQUEST['query'],
-                'user__emails__address__contains' => $_REQUEST['query'],
-                'user__org__name__contains' => $_REQUEST['query'],
-            ));
-            $tickets->filter($basic_search);
-            if (!$_REQUEST['search-type']) {
-                // [Search] click, consider keywords too. This is a
-                // relatively ugly hack. SearchBackend::find() add in a
-                // constraint for the search. We need to pop that off and
-                // include it as an OR with the above constraints
-                $keywords = TicketModel::objects();
-                $keywords->extra(array('select' => array('ticket_id' => 'Z1.ticket_id')));
-                $keywords = $ost->searcher->find($_REQUEST['query'], $keywords);
-                $tickets->values('ticket_id')->annotate(array('__relevance__' => new SqlCode(0.5)));
-                $keywords->aggregated = true; // Hack to prevent select ticket.*
-                $tickets->union($keywords)->order_by(new SqlCode('__relevance__'), QuerySet::DESC);
+        elseif ($_REQUEST['query']) {
+            // [Search] click, consider keywords
+            $__tickets = $ost->searcher->find($_REQUEST['query'], $tickets);
+            if (!count($__tickets)) {
+                // Do wildcard search if no hits
+                $__tickets = $ost->searcher->find($_REQUEST['query'].'*', $tickets);
             }
+            $tickets = $__tickets->distinct('ticket_id');
+            $has_relevance = true;
+        }
+        if (count($tickets) == 1) {
+            // Redirect to ticket page
         }
         // Clear sticky search queue
         unset($_SESSION[$queue_key]);
diff --git a/scp/js/scp.js b/scp/js/scp.js
index 8408453d0dc465d5e33c6097397039a29595ecc9..0b0425e1f8874c93b5abb010412d8e353d36568d 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -746,7 +746,7 @@ $.confirm = function(message, title, options) {
                 .append($('<input type="button" class="close"/>')
                     .attr('value', __('Cancel'))
                     .click(function() { hide(); })
-            )).append($('<span class="buttons pull-right">test</span>')
+            )).append($('<span class="buttons pull-right"></span>')
                 .append($('<input type="button"/>')
                     .attr('value', __('OK'))
                     .click(function() {  hide(); D.resolve(body.find('input').serializeArray()); })