This is relevant to me so I did a bit of work. 
Here is my state that uses reloadOnSearch but fires off a function each time the URL is updated.
      .state('route2.list', {
          url: "/list/:listId",
          templateUrl: "route2.list.html",
          reloadOnSearch: false,
          controller: function($scope, $stateParams){
            console.log($stateParams);
            $scope.listId = $stateParams.listId;
            $scope.things = ["A", "Set", "Of", "Things"];
            //This will fire off every time you update the URL.
            $scope.$on('$locationChangeSuccess', function(event) { 
                $scope.listId = 22;
                console.log("Update");
                console.log($location.url()); 
                console.log($stateParams);    
                });
          }
      }
Returns in console:
Update
/route2/list/7 (index):70
Object {listId: "8"} (index):71
$locationChangeStart and $locationChangeSuccess should get you where you want to go. Keep in mind that your location functions will always fire off, even on the first load.
Inspiration taken from this SO post and the $location documentation
EDIT 2
According to the Angular docs for ngRoute, $routeParams aren't updated until after the success of the changes. Chances are that ui-route has a similar restriction. Since we're inturrupting the resolution of the state, $stateParams is never updated.  What you can do is just emulate how $stateParams are passed in the first place using $urlMatcherFactory.
Like so:
      .state('route2.list', {
          url: "/list/:listId",
          templateUrl: "route2.list.html",
          reloadOnSearch: false,
          controller: function($scope, $stateParams, $state, $urlMatcherFactory){
            console.log($stateParams.listId + " :: " + $location.url());
            $scope.listId = $stateParams.listId;
            $scope.things = ["A", "Set", "Of", "Things"];
            $scope.$on('$locationChangeSuccess', function(event) { 
                $scope.listId = 22;
                //For when you want to dynamically assign arguments for .compile
                console.log($state.get($state.current).url); //returns /list/:listId
                //plain text for the argument for clarity
                var urlMatcher = $urlMatcherFactory.compile("/route2/list/:listId");
                //below returns Object {listId: "11" when url is "/list/11"} 
                console.log(urlMatcher.exec($location.url())); 
                var matched = urlMatcher.exec($location.url());
                //this line will be wrong unless we set $stateParams = matched
                console.log("Update " + $stateParams.listId);
                console.log($location.url());
                //properly updates scope.
                $scope.listId = matched.listId;
                });
          }
      })
The scope actually updates, so that's a good thing. The thing to remember is that you're stoppng the normal resolution of everything but setting reloadOnSearch to false so you'll need to handle pretty much everything on your own.