Index: tractags/api.py
===================================================================
--- tractags/api.py	(revision 10135)
+++ tractags/api.py	(working copy)
@@ -7,8 +7,10 @@
 #
 
 import re
+import copy
 from pkg_resources import resource_filename
 
+from trac.config import BoolOption
 from trac.core import *
 from trac.resource import Resource
 from trac.perm import IPermissionRequestor, PermissionError
@@ -83,6 +85,9 @@
     # Resource realm this provider manages tags for. Set this.
     realm = None
 
+    case_sensitive_queries = BoolOption('tags', 'case_sensitive_queries', True,
+        _("Treat tags differing only in case as distinct."))
+
     # Public methods
     def check_permission(self, perm, operation):
         """Delegate function for checking permissions.
@@ -105,7 +110,10 @@
         args = [self.realm]
         sql = 'SELECT DISTINCT name FROM tags WHERE tagspace=%s'
         if tags:
-            sql += ' AND tags.tag IN (%s)' % ', '.join(['%s' for t in tags])
+            if self.case_sensitive_queries:
+                sql += ' AND tags.tag IN (%s)' % ', '.join(['%s' for t in tags])
+            else:
+                sql += ' AND LOWER(tags.tag) IN (%s)' % ', '.join(['LOWER(%s)' for t in tags])
             args += tags
         sql += ' ORDER by name'
         cursor.execute(sql, args)
@@ -188,12 +196,56 @@
     _tag_split = re.compile('[,\s]+')
     _realm_provider_map = None
 
+    case_sensitive_queries = BoolOption('tags', 'case_sensitive_queries', True,
+        _("Treat tags differing only in case as distinct."))
+
+    cloud_combine_similar = BoolOption('tags', 'cloud_combine_similar', False,
+        _("Cloud should combine tags differing only in case into a single entry."))
+
     def __init__(self):
     # bind the 'tractags' catalog to the specified locale directory
         locale_dir = resource_filename(__name__, 'locale')
         add_domain(self.env.path, locale_dir)
 
 
+    # Helper functions
+    def _casecmp (self, str1, str2):
+    # Case-insensitive string comparison
+      if (str1.lower() == str2.lower()):
+        return True
+      return False
+
+    def _consolidate_taglist (self, taglist):
+      reparse_list = True
+      while reparse_list:
+        reparse_list = False
+        for i in range(0, len(taglist)):
+          # Copy the keys list and remove one specific element
+          keys = taglist.keys()
+          this = copy.deepcopy(keys[i])
+          others = copy.deepcopy(keys)
+          others.remove(keys[i])
+          
+          # If this element appears elsewhere in the list using
+          #   a different case, consolidate them into one
+          merge_list = []
+          for j in range(0, len(others)):
+            if self._casecmp(this, others[j]) == True:
+              merge_list.append(others[j])
+          if len(merge_list) > 0:
+            # Duplicate elements exist, so consolidate them
+            merge_list.append(this)
+            merge_list.sort()
+            for item in merge_list[1:]:
+              taglist[merge_list[0]] += taglist.pop(item)
+            # Restart outer 'for' loop
+            reparse_list = True
+            break
+      
+      # Return modified dictionary
+      return taglist
+
+
     # Public methods
     def query(self, req, query='', attribute_handlers=None):
         """Return a sequence of (resource, tags) tuples matching a query.
@@ -245,6 +297,8 @@
                     all_tags[tag] += 1
                 else:
                     all_tags[tag] = 1
+        if self.cloud_combine_similar:
+            self._consolidate_taglist(all_tags)
         return all_tags
 
     def get_tags(self, req, resource):
Index: tractags/ticket.py
===================================================================
--- tractags/ticket.py	(revision 10135)
+++ tractags/ticket.py	(working copy)
@@ -14,6 +14,7 @@
 from trac.util.compat import set, sorted
 from trac.config import *
 from trac.resource import Resource
+from trac.web.chrome import add_warning
 
 
 class TicketTagProvider(Component):
@@ -29,6 +30,9 @@
     ignore_closed_tickets = BoolOption('tags', 'ignore_closed_tickets', True,
         _("Do not collect tags from closed tickets."))
 
+    case_sensitive_queries = BoolOption('tags', 'case_sensitive_queries', True,
+        _("Treat tags differing only in case as distinct."))
+
 #    custom_fields = ListOption('tags', 'custom_ticket_fields',
 #        doc=_("List of custom ticket fields to expose as tags."))
 
@@ -53,9 +57,13 @@
             ignore)
         constraints = []
         if tags:
-            constraints.append(
-                "(" + ' OR '.join(["fields LIKE %s" for t in tags]) + ")")
-            args += ['%' + t + '%' for t in tags]
+            if self.case_sensitive_queries:
+                constraints.append(
+                    "(" + ' OR '.join(["fields LIKE %s" for t in tags]) + ")")
+                args += ['%' + t + '%' for t in tags]
+            else:
+                constraints.append(
+                    "(" + ' OR '.join(["(LOWER(fields) REGEXP \"[[:<:]]" + t.lower() + "[[:>:]]\")=1" for t in tags]) + ")")
         else:
             constraints.append("fields != ''")
 
@@ -70,8 +78,13 @@
             if 'TICKET_VIEW' not in perm or 'TAGS_VIEW' not in perm:
                 continue
             ticket_tags = split_into_tags(ttags)
-            tags = set([to_unicode(x) for x in tags])
-            if (not tags or ticket_tags.intersection(tags)):
+            if self.case_sensitive_queries:
+                temp_ticket_tags = set([to_unicode(x) for x in ticket_tags])
+                tags = set([to_unicode(x) for x in tags])
+            else:
+                temp_ticket_tags = set([to_unicode(x.lower()) for x in ticket_tags])
+                tags = set([to_unicode(x.lower()) for x in tags])
+            if (not tags or temp_ticket_tags.intersection(tags)):
                 yield Resource('ticket', id), ticket_tags
 
 
Index: tractags/macros.py
===================================================================
--- tractags/macros.py	(revision 10135)
+++ tractags/macros.py	(working copy)
@@ -8,6 +8,7 @@
 
 from genshi.builder import tag as builder
 
+from trac.config import BoolOption
 from trac.core import Component, implements
 from trac.resource import Resource, get_resource_url, render_resource_link
 from trac.util import embedded_numbers
@@ -19,45 +20,6 @@
 from tractags.api import TagSystem, _
 
 
-def render_cloud(env, req, cloud, renderer=None):
-    """Render a tag cloud
-
-    :cloud: Dictionary of {object: count} representing the cloud.
-    :param renderer: A callable with signature (tag, count, percent) used to
-                     render the cloud objects.
-    """
-    min_px = 10.0
-    max_px = 30.0
-    scale = 1.0
-
-    add_stylesheet(req, 'tags/css/tractags.css')
-
-    if renderer is None:
-        def default_renderer(tag, count, percent):
-            href = get_resource_url(env, Resource('tag', tag), req.href)
-            return builder.a(tag, rel='tag', title='%i' % count, href=href,
-                             style='font-size: %ipx' %
-                                   int(min_px + percent * (max_px - min_px)))
-        renderer = default_renderer
-
-    # A LUT from count to n/len(cloud)
-    size_lut = dict([(c, float(i)) for i, c in
-                     enumerate(sorted(set([r for r in cloud.values()])))])
-    if size_lut:
-        scale = 1.0 / len(size_lut)
-
-    ul = builder.ul(class_='tagcloud')
-    last = len(cloud) - 1
-    for i, (tag, count) in enumerate(sorted(cloud.iteritems())):
-        percent = size_lut[count] * scale
-        li = builder.li(renderer(tag, count, percent))
-        if i == last:
-            li(class_='last')
-        li()
-        ul(li, ' ')
-    return ul
-
-
 class TagWikiMacros(Component):
     """Provides macros, that utilize the tagging system in wiki."""
 
@@ -88,6 +50,51 @@
     See tags documentation for the query syntax.
     """)
 
+    cloud_sort_by_case = BoolOption('tags', 'cloud_sort_by_case', True,
+        _("Use case-sensitive sorting for the tag cloud."))
+    
+    def render_cloud(self, env, req, cloud, renderer=None):
+        """Render a tag cloud
+
+        :cloud: Dictionary of {object: count} representing the cloud.
+        :param renderer: A callable with signature (tag, count, percent) used to
+                         render the cloud objects.
+        """
+        min_px = 10.0
+        max_px = 30.0
+        scale = 1.0
+
+        add_stylesheet(req, 'tags/css/tractags.css')
+
+        if renderer is None:
+            def default_renderer(tag, count, percent):
+                href = get_resource_url(env, Resource('tag', tag), req.href)
+                return builder.a(tag, rel='tag', title='%i' % count, href=href,
+                                 style='font-size: %ipx' %
+                                       int(min_px + percent * (max_px - min_px)))
+            renderer = default_renderer
+
+        # A LUT from count to n/len(cloud)
+        size_lut = dict([(c, float(i)) for i, c in
+                         enumerate(sorted(set([r for r in cloud.values()])))])
+        if size_lut:
+            scale = 1.0 / len(size_lut)
+
+        ul = builder.ul(class_='tagcloud')
+        last = len(cloud) - 1
+        if self.cloud_sort_by_case:
+            enum = enumerate(sorted(cloud.iteritems()))
+        else:
+            enum = enumerate(sorted(cloud.iteritems(), key=lambda x: x[0].lower()))
+        for i, (tag, count) in enum:
+            percent = size_lut[count] * scale
+            li = builder.li(renderer(tag, count, percent))
+            if i == last:
+                li(class_='last')
+            li()
+            ul(li, ' ')
+        return ul
+
     # IWikiMacroProvider
 
     def get_macros(self):
@@ -106,7 +113,7 @@
                 content = ''
             req = formatter.req
             all_tags = TagSystem(self.env).get_all_tags(req, content)
-            return render_cloud(self.env, req, all_tags)
+            return self.render_cloud(self.env, req, all_tags)
 
         elif name == 'ListTagged':
             req = formatter.req
Index: tractags/query.py
===================================================================
--- tractags/query.py	(revision 10135)
+++ tractags/query.py	(working copy)
@@ -11,6 +11,7 @@
 """
 
 import re
+from trac.config import BoolOption
 from trac.core import TracError
 from tractags.api import _
 
@@ -153,6 +154,9 @@
                   'or': QueryNode.OR, 'attr': QueryNode.ATTR,
                   'startsub': QueryNode.BEGINSUB, 'endsub': QueryNode.ENDSUB}
 
+    case_sensitive_queries = BoolOption('tags', 'case_sensitive_queries', True,
+        _("Treat tags differing only in case as distinct."))
+
     def __init__(self, phrase, attribute_handlers=None):
         """Construct a new Query.
 
@@ -272,7 +276,10 @@
             if not node or node.type == node.NULL:
                 return True
             elif node.type == node.TERM:
-                return node.value in terms
+                if self.case_sensitive_queries:
+                    return node.value in terms
+                else:
+                    return node.value.lower() in [x.lower() for x in terms]
             elif node.type == node.AND:
                 return _match(node.left) and _match(node.right)
             elif node.type == node.OR:
