Why is the second-order-section cascade approach giving "worse" results?
I don't think that has anything to do with SOS vs transfer function representation, but it appears to be an artifact how filtfilt()  handles initial conditions. The documentation is annoyingly vague about this and simply says it 


  filtfilt minimizes start-up and ending transients by matching initial
  conditions.


If you implement filtfilt() manually you get the expected result: both results are basically correct, but SOS has less  numerical noise. See code snippet below.

In general, using filtfilt() is tricky: ideally you have an input signal that has lots of zeros at the beginning and the end, where "lots" means more than the length of the impulse response. Otherwise the results are highly dependent on the initial states of the filters and how you manage them.

Numerically, SOS is always better. Take a look at your "a" coefficients: the ratio of the largest to smallest coefficient is in the order of 1e10. Adding numbers with that different in magnitude creates numerically issues, even if you use double precision. For the SOS representation, that ratios is only about 14, that's about 9 orders of magnitude better!

%% signal
t = (0:.08:100)';
x = sin(2*pi*t);

%% original
n=15;
Wn=[0.5 3]/6.25;
[z,p,k]=butter(n,Wn);
[sos,g] = zp2sos(z,p,k);
d1=filtfilt(sos,g,x);
[b,a]=butter(n,Wn);
d2=filtfilt(b,a,x);

%% manual implementation of filtfilt()
y = filter(b,a,x);
y1 = filter(b,a,flipud(y));
y = g*sosfilt(sos,x);
y2 = g*sosfilt(sos,flipud(y));
plot([y2 y2-y1]); legend('result','difference');